Merge remote-tracking branch 'github/main' into protonj2-contribution
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..78bb807
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,30 @@
+# This workflow will build a Java project with Maven
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: "Build"
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-18.04
+    strategy:
+      fail-fast: false
+      matrix:
+        java: [ 11, 16 ]
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/cache@v2
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven-
+      - name: Install JDK ${{ matrix.java }}
+        uses: actions/setup-java@v1
+        with:
+          java-version: ${{ matrix.java }}
+
+      - name: Build
+        run: mvn -B clean verify
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7676a3b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+*~
+*.swp
+
+# Start of IntelliJ IDE files
+.idea
+.idea/*
+*.iml
+*.ipr
+*.iws
+# End of IntelliJ IDE files
+
+/build/
+*.class
+*.pyc
+*.pyo
+target
+
+.DS_Store
+
+# Start of Eclipse IDE files
+.project
+.classpath
+.settings
+.cproject
+eclipse-classes
+# End of Eclipse IDE files
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6b0b127
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,203 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..ac2b343
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache Qpid Proton4J
+Copyright 2012-2021 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
index f221241..844d6e7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,13 @@
-# qpid-protonj2
+# Apache Qpid proton-j2
+=======================
+
+[![Linux Build Status](https://travis-ci.org/apache/qpid-protonj2.svg?branch=master)](https://travis-ci.org/apache/qpid-protonj2)
+[![Windows Build Status](https://ci.appveyor.com/api/projects/status/wh587qrxa3c22mh2/branch/master?svg=true)](https://ci.appveyor.com/project/ApacheSoftwareFoundation/qpid-protonj2/branch/master)
+
+
+Qpid proton-j2 is a high-performance, lightweight AMQP protocol library. It can be
+used in the widest range of messaging applications, including brokers, client
+libraries, routers, bridges, proxies, and more.
+
+Please see http://qpid.apache.org/proton for more information.
+
diff --git a/apache-qpid-protonj2/README.md b/apache-qpid-protonj2/README.md
new file mode 100644
index 0000000..d77b1d9
--- /dev/null
+++ b/apache-qpid-protonj2/README.md
@@ -0,0 +1 @@
+This module is used to produce the Apache Qpid proton-j2 release assemblies.
diff --git a/apache-qpid-protonj2/pom.xml b/apache-qpid-protonj2/pom.xml
new file mode 100644
index 0000000..697264e
--- /dev/null
+++ b/apache-qpid-protonj2/pom.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+--><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/maven-v4_0_0.xsd">
+  <parent>
+    <groupId>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>apache-qpid-protonj2</artifactId>
+  <packaging>pom</packaging>
+  <name>Apache Qpid protonj2</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2-client</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+      <optional>true</optional>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>make-assembly</id>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+            <configuration>
+              <descriptors>
+                <descriptor>src/main/assembly/bin.xml</descriptor>
+              </descriptors>
+              <tarLongFileMode>gnu</tarLongFileMode>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>apache-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-assembly-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>make-src-assembly</id>
+                <phase>package</phase>
+                <goals>
+                  <goal>single</goal>
+                </goals>
+                <configuration>
+                  <descriptors>
+                    <descriptor>src/main/assembly/src.xml</descriptor>
+                  </descriptors>
+                  <tarLongFileMode>gnu</tarLongFileMode>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+
+          <!-- Calculate checksum for Apache dist area. Overrides the
+               apache parent pom execution configuration, for this module -->
+          <plugin>
+            <groupId>net.nicoulaj.maven.plugins</groupId>
+            <artifactId>checksum-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>source-release-checksum</id>
+                <goals>
+                  <goal>files</goal>
+                </goals>
+                <configuration>
+                  <fileSets>
+                    <fileSet>
+                      <directory>${project.build.directory}</directory>
+                      <includes>
+                        <include>apache-qpid-jms-${project.version}-src.tar.gz</include>
+                        <include>apache-qpid-jms-${project.version}-bin.tar.gz</include>
+                      </includes>
+                    </fileSet>
+                  </fileSets>
+                  <failIfNoFiles>true</failIfNoFiles>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/apache-qpid-protonj2/src/main/assembly/LICENSE b/apache-qpid-protonj2/src/main/assembly/LICENSE
new file mode 100644
index 0000000..6b0b127
--- /dev/null
+++ b/apache-qpid-protonj2/src/main/assembly/LICENSE
@@ -0,0 +1,203 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
diff --git a/apache-qpid-protonj2/src/main/assembly/NOTICE b/apache-qpid-protonj2/src/main/assembly/NOTICE
new file mode 100644
index 0000000..ac2b343
--- /dev/null
+++ b/apache-qpid-protonj2/src/main/assembly/NOTICE
@@ -0,0 +1,5 @@
+Apache Qpid Proton4J
+Copyright 2012-2021 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/apache-qpid-protonj2/src/main/assembly/bin.xml b/apache-qpid-protonj2/src/main/assembly/bin.xml
new file mode 100644
index 0000000..ce128da
--- /dev/null
+++ b/apache-qpid-protonj2/src/main/assembly/bin.xml
@@ -0,0 +1,50 @@
+<?xml version='1.0'?>
+<!--
+
+ 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.
+
+-->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+  <id>bin</id>
+  <formats>
+    <format>tar.gz</format>
+  </formats>
+  <baseDirectory>apache-qpid-protonj2-${project.version}</baseDirectory>
+  <fileSets>
+    <fileSet>
+      <directory>${basedir}/src/main/assembly/</directory>
+      <outputDirectory>/</outputDirectory>
+      <includes>
+        <include>NOTICE</include>
+        <include>LICENSE</include>
+      </includes>
+      <fileMode>0644</fileMode>
+      <directoryMode>0755</directoryMode>
+    </fileSet>
+  </fileSets>
+  <dependencySets>
+    <dependencySet>
+      <outputDirectory>/lib</outputDirectory>
+      <useProjectArtifact>false</useProjectArtifact>
+      <useTransitiveFiltering>true</useTransitiveFiltering>
+    </dependencySet>
+  </dependencySets>
+</assembly>
+
diff --git a/apache-qpid-protonj2/src/main/assembly/src.xml b/apache-qpid-protonj2/src/main/assembly/src.xml
new file mode 100644
index 0000000..71e5862
--- /dev/null
+++ b/apache-qpid-protonj2/src/main/assembly/src.xml
@@ -0,0 +1,81 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!--
+  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.
+-->
+
+<assembly>
+  <id>src</id>
+  <formats>
+    <format>tar.gz</format>
+  </formats>
+  <baseDirectory>apache-qpid-protonj2-${project.version}-src</baseDirectory>
+  <fileSets>
+    <!-- main project directory structure -->
+    <fileSet>
+      <directory>${project.basedir}/..</directory>
+      <outputDirectory>/</outputDirectory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+      <excludes>
+        <!-- build output -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/).*${project.build.directory}.*]</exclude>
+
+        <!-- NOTE: Most of the following excludes should not be required
+             if the standard release process is followed. This is because the
+             release plugin checks out project sources into a location like
+             target/checkout, then runs the build from there. The result is
+             a source-release archive that comes from a pretty clean directory
+             structure.
+
+             HOWEVER, if the release plugin is configured to run extra goals
+             or generate a project website, it's definitely possible that some
+             of these files will be present. So, it's safer to exclude them.
+        -->
+
+        <!-- IDEs -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?maven-eclipse\.xml]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.project]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.classpath]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?[^/]*\.iws]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.idea(/.*)?]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?out(/.*)?]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?[^/]*\.ipr]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?[^/]*\.iml]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.settings(/.*)?]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.externalToolBuilders(/.*)?]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.deployables(/.*)?]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.wtpmodules(/.*)?]</exclude>
+
+        <!-- misc -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?cobertura\.ser]</exclude>
+
+        <!-- release-plugin temp files -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?pom\.xml\.releaseBackup]</exclude>
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?release\.properties]</exclude>
+
+        <!-- Jython class files -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?.*\$py\.class]</exclude>
+
+        <!-- Git mailmap file -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.mailmap]</exclude>
+
+        <!-- jenkins workspace local repo -->
+        <exclude>%regex[(?!((?!${project.build.directory}/)[^/]+/)*src/)(.*/)?\.repository(/.*)?]</exclude>
+      </excludes>
+    </fileSet>
+  </fileSets>
+</assembly>
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..836eece
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,35 @@
+version: '{build}'
+skip_tags: true
+clone_depth: 30
+
+environment:
+  JAVA_HOME: C:\Program Files\Java\jdk1.8.0
+
+install:
+  - cmd: SET PATH=%JAVA_HOME%\bin;%PATH%
+
+build_script:
+  - mvn clean install -B -DskipTests
+
+test_script:
+  - mvn clean install -B
+
+on_failure:
+  - ps: |
+      7z a -r surefire-reports.zip '**\target\surefire-reports\*'
+      Push-AppveyorArtifact surefire-reports.zip -DeploymentName 'Surefire Reports'
+
+on_finish:
+  - ps: |
+      $url = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)"
+      $wc = New-Object 'System.Net.WebClient'
+      $dirs = Get-ChildItem -Filter surefire-reports -Recurse
+      ForEach ($dir in $dirs)
+      {
+        $files = Get-ChildItem -Path $dir.FullName -Filter TEST-*.xml
+        ForEach ($file in $files)
+        {
+          $wc.UploadFile($url, (Resolve-Path $file.FullName))
+        }
+      }
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..931ec9e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,385 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+--><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/maven-v4_0_0.xsd">
+
+  <parent>
+    <groupId>org.apache</groupId>
+    <artifactId>apache</artifactId>
+    <version>23</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.qpid</groupId>
+  <artifactId>protonj2-parent</artifactId>
+  <version>0.1.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Qpid protonj2 Parent</name>
+  <description>Qpid protonj2 is a library for speaking AMQP 1.0.</description>
+
+  <properties>
+    <!-- See also maven.compiler.release in the java9on profile -->
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+
+    <!-- Test dependency versions -->
+    <junit.jupiter.version>5.7.1</junit.jupiter.version>
+    <junit.vintage.version>5.7.1</junit.vintage.version>
+    <mockito.version>3.8.0</mockito.version>
+    <proton.version>0.33.8</proton.version>
+    <slf4j.version>1.7.30</slf4j.version>
+    <log4j.slf4j.version>2.14.1</log4j.slf4j.version>
+    <hamcrest.version>2.2</hamcrest.version>
+    <netty.version>4.1.63.Final</netty.version>
+    <netty.iouring.version>0.0.5.Final</netty.iouring.version>
+    <netty.tcnative.version>2.0.38.Final</netty.tcnative.version>
+
+    <!-- Plugin versions -->
+    <maven.bundle.plugin.version>5.1.1</maven.bundle.plugin.version>
+    <jacoco.plugin.version>0.8.6</jacoco.plugin.version>
+
+    <!-- Test properties -->
+    <maven.test.redirectTestOutputToFile>true</maven.test.redirectTestOutputToFile>
+    <surefire.runOrder>filesystem</surefire.runOrder>
+    <proton.trace.frames>false</proton.trace.frames>
+
+    <netty-transport-native-io-uring-classifier>linux-x86_64</netty-transport-native-io-uring-classifier>
+    <netty-transport-native-epoll-classifier>linux-x86_64</netty-transport-native-epoll-classifier>
+    <netty-transport-native-kqueue-classifier>osx-x86_64</netty-transport-native-kqueue-classifier>
+
+    <!-- surefire forked jvm arguments -->
+    <argLine>-Xmx2g -enableassertions ${jacoco-config}</argLine>
+  </properties>
+
+  <url>http://qpid.apache.org/proton</url>
+  <scm>
+    <connection>scm:git:http://git-wip-us.apache.org/repos/asf/qpid-protonj2.git</connection>
+    <developerConnection>scm:git:https://git-wip-us.apache.org/repos/asf/qpid-protonj2.git</developerConnection>
+    <url>https://git-wip-us.apache.org/repos/asf?p=qpid-protonj2.git</url>
+    <tag>HEAD</tag>
+  </scm>
+  <issueManagement>
+    <url>https://issues.apache.org/jira/browse/PROTON</url>
+    <system>JIRA</system>
+  </issueManagement>
+  <ciManagement>
+    <url>https://builds.apache.org/view/M-R/view/Qpid/job/Qpid-protonj2/</url>
+  </ciManagement>
+
+  <modules>
+    <module>protonj2</module>
+    <module>protonj2-performance-tests</module>
+    <module>protonj2-test-driver</module>
+    <module>protonj2-client</module>
+    <module>protonj2-client-examples</module>
+    <module>protonj2-client-docs</module>
+    <module>apache-qpid-protonj2</module>
+  </modules>
+
+  <dependencyManagement>
+    <dependencies>
+      <!-- Internal module dependencies -->
+      <dependency>
+        <groupId>org.apache.qpid</groupId>
+        <artifactId>protonj2</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.qpid</groupId>
+        <artifactId>protonj2-test-driver</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.qpid</groupId>
+        <artifactId>protonj2-client</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4j.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-slf4j-impl</artifactId>
+        <version>${log4j.slf4j.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-buffer</artifactId>
+        <version>${netty.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-common</artifactId>
+        <version>${netty.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-handler</artifactId>
+        <version>${netty.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-transport</artifactId>
+        <version>${netty.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-codec-http</artifactId>
+        <version>${netty.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-transport-native-epoll</artifactId>
+        <version>${netty.version}</version>
+        <classifier>${netty-transport-native-epoll-classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-transport-native-kqueue</artifactId>
+        <version>${netty.version}</version>
+        <classifier>${netty-transport-native-kqueue-classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>io.netty.incubator</groupId>
+        <artifactId>netty-incubator-transport-native-io_uring</artifactId>
+        <version>${netty.iouring.version}</version>
+        <classifier>${netty-transport-native-io-uring-classifier}</classifier>
+      </dependency>
+      <!--  Testing only Uber Jar inclusion -->
+      <dependency>
+         <groupId>io.netty</groupId>
+         <artifactId>netty-tcnative-boringssl-static</artifactId>
+         <version>${netty.tcnative.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.qpid</groupId>
+        <artifactId>proton-j</artifactId>
+        <version>${proton.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-api</artifactId>
+        <version>${junit.jupiter.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-engine</artifactId>
+        <version>${junit.jupiter.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-params</artifactId>
+        <version>${junit.jupiter.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.hamcrest</groupId>
+        <artifactId>hamcrest</artifactId>
+        <version>${hamcrest.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.hamcrest</groupId>
+        <artifactId>hamcrest-library</artifactId>
+        <version>${hamcrest.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.mockito</groupId>
+        <artifactId>mockito-core</artifactId>
+        <version>${mockito.version}</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <defaultGoal>install</defaultGoal>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <optimize>true</optimize>
+          <showDeprecation>true</showDeprecation>
+          <showWarnings>true</showWarnings>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <version>${maven.bundle.plugin.version}</version>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Export-Package>${project.groupId}.protonj2.*</Export-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <configuration>
+          <excludes combine.children="append">
+            <exclude>appveyor.yml</exclude>
+            <exclude>.travis.yml</exclude>
+            <exclude>.mailmap</exclude>
+            <exclude>**/*.md</exclude>
+            <exclude>**/*.pkcs12</exclude>
+            <exclude>**/*.p12</exclude>
+            <exclude>**/*.pem</exclude>
+            <exclude>**/*.pem.txt</exclude>
+            <exclude>**/*.crt</exclude>
+            <exclude>**/*.csr</exclude>
+            <exclude>**/*.keystore</exclude>
+            <exclude>**/*.truststore</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>prepare-agent</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <propertyName>jacoco-config</propertyName>
+        </configuration>
+      </plugin>
+    </plugins>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <configuration>
+            <runOrder>${surefire.runOrder}</runOrder>
+            <redirectTestOutputToFile>${maven.test.redirectTestOutputToFile}</redirectTestOutputToFile>
+            <forkCount>1</forkCount>
+            <reuseForks>true</reuseForks>
+            <systemPropertyVariables>
+                <java.awt.headless>true</java.awt.headless>
+            </systemPropertyVariables>
+            <failIfNoTests>false</failIfNoTests>
+            <environmentVariables>
+              <PN_TRACE_FRM>${proton.trace.frames}</PN_TRACE_FRM>
+            </environmentVariables>
+          </configuration>
+        </plugin>
+        <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.apache.felix</groupId>
+                    <artifactId>maven-bundle-plugin</artifactId>
+                    <versionRange>[2.4.0,)</versionRange>
+                    <goals>
+                      <goal>manifest</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore/>
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-release-plugin</artifactId>
+          <configuration>
+            <autoVersionSubmodules>true</autoVersionSubmodules>
+            <tagNameFormat>@{project.version}</tagNameFormat>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.jacoco</groupId>
+          <artifactId>jacoco-maven-plugin</artifactId>
+          <version>${jacoco.plugin.version}</version>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <reporting>
+    <plugins>
+      <plugin>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-maven-plugin</artifactId>
+        <version>${spotbugs-maven-plugin-version}</version>
+      </plugin>
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <version>${jacoco.plugin.version}</version>
+      </plugin>
+    </plugins>
+  </reporting>
+
+  <profiles>
+    <!-- Override the apache-release profile from the parent. Skip creating
+         a source release here, we have a release module that does it.  -->
+    <profile>
+      <id>apache-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-assembly-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>source-release-assembly</id>
+                <configuration>
+                  <skipAssembly>true</skipAssembly>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <profile>
+      <id>sources</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-source-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>attach-sources</id>
+                <goals>
+                  <goal>jar</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+</project>
diff --git a/protonj2-client-docs/Configuration.md b/protonj2-client-docs/Configuration.md
new file mode 100644
index 0000000..2bed42b
--- /dev/null
+++ b/protonj2-client-docs/Configuration.md
@@ -0,0 +1,108 @@
+# Qpid protonj2 Imperative Client configuration
+
+This file details various configuration options for the Imperative API based Java client.  Each of the resources
+that allow configuration accept a configuration options object that encapsulates all configuration for that specific
+resuource.
+
+## Client Options
+
+Before creating a new connection a Client object is created which accept a ClientOptions object to configure it.
+
++ **ClientOptions.id** Allows configuration of the AMQP Container Id used by newly created Connections, if none is set the Client instance will create a unique Container Id that will be assigned to all new connections.
+
+## Connection Configuration Options
+
+The ConnectionOptions object can be provided to a Client instance when creating a new connection and allows configuration of several different aspects of the resulting Connection instance.
+
++ **ConnectionOptions.username** User name value used to authenticate the connection
++ **ConnectionOptions.password** The password value used to authenticate the connection
++ **ConnectionOptions.sslEnabled** A connection level convenience option that enables or disables the transport level SSL functionality.  See the connection transport options for more details on SSL configuration, if nothing is configures the connection will atempt to configure the SSL transport using the standard system level configuration properties.
++ **ConnectionOptions.closeTimeout** Timeout value that controls how long the client connection waits on resource closure before returning. By default the client waits 60 seconds for a normal close completion event.
++ **ConnectionOptions.sendTimeout** Timeout value that controls how long the client connection waits on completion of a synchronous message send before returning an error. By default the client will wait indefinitely for a send to complete.
++ **ConnectionOptions.openTimeout** Timeout value that controls how long the client connection waits on the AMQP Open process to complete  before returning with an error. By default the client waits 15 seconds for a connection to be established before failing.
++ **ConnectionOptions.requestTimeout** Timeout value that controls how long the client connection waits on completion of various synchronous interactions, such as initiating or retiring a transaction, before returning an error. Does not affect synchronous message sends. By default the client will wait indefinitely for a request to complete.
++ **ConnectionOptions.drainTimeout** Timeout value that controls how long the client connection waits on completion of a drain request for a Receiver link before failing that request with an error.  By default the client waits 60 seconds for a normal link drained completion event.
++ **ConnectionOptions.virtualHost** The vhost to connect to. Used to populate the Sasl and Open hostname fields. Default is the main hostname from the hostname provided when opening the Connection.
++ **ConnectionOptions.traceFrames** Configure if the newly created connection should enabled AMQP frame tracing to the system output.
+
+### Connection Transport Options
+
+The ConnectionOptions object exposes a set of configuration options for the underlying I/O transport layer known as the TransportOptions which allows for fine grained configuration of network level options.
+
++ **transportOptions.sendBufferSize** default is 64k
++ **transportOptions.receiveBufferSize** default is 64k
++ **transportOptions.trafficClass** default is 0
++ **transportOptions.connectTimeout** default is 60 seconds
++ **transportOptions.soTimeout** default is -1
++ **transportOptions.soLinger** default is -1
++ **transportOptions.tcpKeepAlive** default is false
++ **transportOptions.tcpNoDelay** default is true
++ **transportOptions.allowNativeIO** When true the transport will use a native IO transport implementations such as Epoll or KQueue when available instead of the NIO layer, which can improve performance. Defaults to true.
++ **transportOptions.useWebSockets** should the client use a Web Socket based transport layer when establishing connections, default is false
+
+### Connection SSL Options
+
+If an secure connection is desired the ConnectionOptions exposes another options type for configuring the client for that, the SslOptions.
+
++ **sslOptions.useSsl** Enables or disables the use of the SSL transport layer, default is false.
++ **sslOptions.keyStoreLocation**  default is to read from the system property "javax.net.ssl.keyStore"
++ **sslOptions.keyStorePassword**  default is to read from the system property "javax.net.ssl.keyStorePassword"
++ **sslOptions.trustStoreLocation**  default is to read from the system property "javax.net.ssl.trustStore"
++ **sslOptions.trustStorePassword**  default is to read from the system property "javax.net.ssl.trustStorePassword"
++ **sslOptions.keyStoreType** The type of keyStore being used. Default is to read from the system property "javax.net.ssl.keyStoreType" If not set then default is "JKS".
++ **sslOptions.trustStoreType** The type of trustStore being used. Default is to read from the system property "javax.net.ssl.trustStoreType" If not set then default is "JKS".
++ **sslOptions.storeType** This will set both the keystoreType and trustStoreType to the same value. If not set then the keyStoreType and trustStoreType will default to the values specified above.
++ **sslOptions.contextProtocol** The protocol argument used when getting an SSLContext. Default is TLS, or TLSv1.2 if using OpenSSL.
++ **sslOptions.enabledCipherSuites** The cipher suites to enable, comma separated. No default, meaning the context default ciphers are used. Any disabled ciphers are removed from this.
++ **sslOptions.disabledCipherSuites** The cipher suites to disable, comma separated. Ciphers listed here are removed from the enabled ciphers. No default.
++ **sslOptions.enabledProtocols** The protocols to enable, comma separated. No default, meaning the context default protocols are used. Any disabled protocols are removed from this.
++ **sslOptions.disabledProtocols** The protocols to disable, comma separated. Protocols listed here are removed from the enabled protocols. Default is "SSLv2Hello,SSLv3".
++ **sslOptions.trustAll** Whether to trust the provided server certificate implicitly, regardless of any configured trust store. Defaults to false.
++ **sslOptions.verifyHost** Whether to verify that the hostname being connected to matches with the provided server certificate. Defaults to true.
++ **sslOptions.keyAlias** The alias to use when selecting a keypair from the keystore if required to send a client certificate to the server. No default.
++ **sslOptions.allowNativeSSL** When true the transport will attempt to use native OpenSSL libraries for SSL connections if possible based on the SSL configuration and available OpenSSL libraries on the classpath.
+
+### Connection Automatic Reconnect Options
+
+* **reconnectionOptions.reconnectEnabled** enables connection level reconnect for the client, default is false.
++ **reconnectionOptions.reconnectDelay** Controls the delay between successive reconnection attempts, defaults to 10 milliseconds.  If the backoff option is not enabled this value remains constant.
++ **reconnectionOptions.maxReconnectDelay** The maximum time that the client will wait before attempting a reconnect.  This value is only used when the backoff feature is enabled to ensure that the delay doesn't not grow too large.  Defaults to 30 seconds as the max time between connect attempts.
++ **reconnectionOptions.useReconnectBackOff** Controls whether the time between reconnection attempts should grow based on a configured multiplier.  This option defaults to true.
++ **reconnectionOptions.reconnectBackOffMultiplier** The multiplier used to grow the reconnection delay value, defaults to 2.0d.
++ **reconnectionOptions.maxReconnectAttempts** The number of reconnection attempts allowed before reporting the connection as failed to the client.  The default is no limit or (-1).
++ **reconnectionOptions.maxInitialConnectionAttempts** For a client that has never connected to a remote peer before this option control how many attempts are made to connect before reporting the connection as failed.  The default is to use the value of maxReconnectAttempts.
++ **reconnectionOptions.warnAfterReconnectAttempts** Controls how often the client will log a message indicating that failover reconnection is being attempted.  The default is to log every 10 connection attempts.
+
+## Session Configuration Options
+
++ **SessionOptions.closeTimeout** Timeout value that controls how long the client session waits on resource closure before returning. By default the client uses the matching connection level close timeout option value.
++ **SessionOptions.sendTimeout** Timeout value that sets the Session level default send timeout which can control how long a Sender waits on completion of a synchronous message send before returning an error. By default the client uses the matching connection level send timeout option value.
++ **SessionOptions.openTimeout** Timeout value that controls how long the client Session waits on the AMQP Open process to complete  before returning with an error. By default the client uses the matching connection level close timeout option value.
++ **SessionOptions.requestTimeout** Timeout value that controls how long the client connection waits on completion of various synchronous interactions, such as initiating or retiring a transaction, before returning an error. Does not affect synchronous message sends. By default the client uses the matching connection level request timeout option value.
++ **SessionOptions.drainTimeout** Timeout value that controls how long the Receiver create by this Session waits on completion of a drain request before failing that request with an error.  By default the client uses the matching connection level drain timeout option value.
+
+## Sender Configuration Options
+
++ **SenderOptions.closeTimeout** Timeout value that controls how long the client Sender waits on resource closure before returning. By default the client uses the matching session level close timeout option value.
++ **SenderOptions.sendTimeout** Timeout value that sets the Sender default send timeout which can control how long a Sender waits on completion of a synchronous message send before returning an error. By default the client uses the matching session level send timeout option value.
++ **SenderOptions.openTimeout** Timeout value that controls how long the client Sender waits on the AMQP Open process to complete  before returning with an error. By default the client uses the matching session level close timeout option value.
++ **SenderOptions.requestTimeout** Timeout value that controls how long the client connection waits on completion of various synchronous interactions, such as initiating or retiring a transaction, before returning an error. Does not affect synchronous message sends. By default the client uses the matching session level request timeout option value.
+
+## Receiver Configuration Options
+
++ **ReceiverOptions.creditWindow** Configures the size of the credit window the Receiver will open with the remote which the Receiver will replenish automatically as incoming deliveries are read.  The default value is 10, to disable and control credit manually this value should be set to zero.
++ **ReceiverOptions.closeTimeout** Timeout value that controls how long the client session waits on resource closure before returning. By default the client uses the matching session level close timeout option value.
++ **ReceiverOptions.openTimeout** Timeout value that controls how long the client Session waits on the AMQP Open process to complete  before returning with an error. By default the client uses the matching session level close timeout option value.
++ **ReceiverOptions.requestTimeout** Timeout value that controls how long the client Receiver waits on completion of various synchronous interactions, such settlement of a delivery, before returning an error. By default the client uses the matching session level request timeout option value.
++ **ReceiverOptions.drainTimeout** Timeout value that controls how long the Receiver link waits on completion of a drain request before failing that request with an error.  By default the client uses the matching session level drain timeout option value.
+
+## Logging
+
+The client makes use of the SLF4J API, allowing users to select a particular logging implementation based on their needs by supplying a SLF4J 'binding', such as *slf4j-log4j* in order to use Log4J. More details on SLF4J are available from http://www.slf4j.org/.
+
+The client uses Logger names residing within the *org.apache.qpid.protonj2* hierarchy, which you can use to configure a logging implementation based on your needs.
+
+When debugging some issues, it may sometimes be useful to enable additional protocol trace logging from the Qpid Proton AMQP 1.0 library. There are two options to achieve this:
+
++ Set the environment variable (not Java system property) *PN_TRACE_FRM* to *true*, which will cause Proton to emit frame logging to stdout.
++ Setting the option *ConnectionOptions.traceFrames=true* to your connection options object to have the client add a protocol tracer to Proton, and configure the *org.apache.qpid.protonj2.engine.impl.ProtonFrameLoggingHandler* Logger to *TRACE* level to include the output in your logs.
diff --git a/protonj2-client-docs/README.md b/protonj2-client-docs/README.md
new file mode 100644
index 0000000..ae53937
--- /dev/null
+++ b/protonj2-client-docs/README.md
@@ -0,0 +1,7 @@
+# Qpid protonj2 client documentation
+
+The docs are raw Markdown right now, we still need to put stuff in place to convert
+to other formats like HTML.
+
+Until then you might find it easier to view them by browsing the GitHub repository:
+https://github.com/apache/qpid-protonj2
diff --git a/protonj2-client-docs/pom.xml b/protonj2-client-docs/pom.xml
new file mode 100644
index 0000000..548a494
--- /dev/null
+++ b/protonj2-client-docs/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+--><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/maven-v4_0_0.xsd">
+  <parent>
+    <groupId>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>protonj2-client-docs</artifactId>
+  <packaging>pom</packaging>
+  <name>Qpid protonj2 Client Documentation</name>
+
+  <properties>
+    <jacoco.skip>true</jacoco.skip>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
+
diff --git a/protonj2-client-examples/README.md b/protonj2-client-examples/README.md
new file mode 100644
index 0000000..ea875ce
--- /dev/null
+++ b/protonj2-client-examples/README.md
@@ -0,0 +1,28 @@
+# Running the client examples
+----------------------------------------------
+
+Use maven to build the module, and additionally copy the dependencies
+alongside their output:
+
+    mvn clean package dependency:copy-dependencies -DincludeScope=runtime -DskipTests
+
+Now you can run the examples using commands of the format:
+
+    Linux:   java -cp "target/classes/:target/dependency/*" org.apache.qpid.protonj2.client.examples.HelloWorld
+
+    Windows: java -cp "target\classes\;target\dependency\*" org.apache.qpid.protonj2.client.examples.HelloWorld
+
+NOTE: The examples expect to use a Queue named "queue". You may need to create
+this before running the examples, depending on the broker/peer you are using.
+
+NOTE: By default the examples can only connect anonymously. A username and
+password with which the connection can authenticate with the server may be set
+through system properties named USER and PASSWORD respectively. E.g:
+
+    Linux:   java -DUSER=guest -DPASSWORD=guest -cp "target/classes/:target/dependency/*" org.apache.qpid.protonj2.client.examples.HelloWorld
+
+    Windows: java -DUSER=guest -DPASSWORD=guest -cp "target\classes\;target\dependency\*" org.apache.qpid.protonj2.client.examples.HelloWorld
+
+NOTE: The earlier build command will cause Maven to resolve the client artifact
+dependencies against its local and remote repositories. If you wish to use a
+locally-built client, ensure to "mvn install" it in your local repo first.
diff --git a/protonj2-client-examples/pom.xml b/protonj2-client-examples/pom.xml
new file mode 100644
index 0000000..c6fa049
--- /dev/null
+++ b/protonj2-client-examples/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<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>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>protonj2-client-examples</artifactId>
+  <name>Qpid protonj2 Client Examples</name>
+  <description>Examples showing the use of the AMQP client</description>
+  <packaging>jar</packaging>
+
+  <properties>
+    <jacoco.skip>true</jacoco.skip>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2-client</artifactId>
+    </dependency>
+
+    <!-- Provide a logging implementation to avoid
+         notice from SLF4J that none was found -->
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <configuration>
+          <!-- Skip deploying the examples, the source is what is
+               useful and will be bundled with the main assembly -->
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/HelloWorld.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/HelloWorld.java
new file mode 100644
index 0000000..7e54a3e
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/HelloWorld.java
@@ -0,0 +1,50 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.Sender;
+
+public class HelloWorld {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "hello-world-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            Receiver receiver = connection.openReceiver(address);
+
+            Sender sender = connection.openSender(address);
+            sender.send(Message.create("Hello World"));
+
+            Delivery delivery = receiver.receive();
+            Message<String> received = delivery.message();
+            System.out.println("Received message with body: " + received.body());
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/LargeMessageReceiver.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/LargeMessageReceiver.java
new file mode 100644
index 0000000..06b92ae
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/LargeMessageReceiver.java
@@ -0,0 +1,57 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import java.io.InputStream;
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.StreamDelivery;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.StreamReceiverMessage;
+
+public class LargeMessageReceiver {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "large-message-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            StreamReceiver receiver = connection.openStreamReceiver(address);
+            StreamDelivery delivery = receiver.receive();
+            StreamReceiverMessage message = delivery.message();
+            InputStream inputStream = message.body();
+
+            byte[] chunk = new byte[10];
+            int readCount = 0;
+
+            while (inputStream.read(chunk) != -1) {
+                System.out.println(String.format("Read data chunk [%2d]: %s", ++readCount, Arrays.toString(chunk)));
+            }
+
+            inputStream.close();
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/LargeMessageSender.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/LargeMessageSender.java
new file mode 100644
index 0000000..89f2f7a
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/LargeMessageSender.java
@@ -0,0 +1,66 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.OutputStreamOptions;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderMessage;
+
+public class LargeMessageSender {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "large-message-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            StreamSender sender = connection.openStreamSender(address);
+            StreamSenderMessage message = sender.beginMessage();
+
+            final byte[] buffer = new byte[100];
+            Arrays.fill(buffer, (byte) 'A');
+
+            message.durable(true);
+
+            // Creates an OutputStream that writes a single Data Section whose expected
+            // size is configured in the stream options.
+            OutputStreamOptions streamOptions = new OutputStreamOptions().bodyLength(buffer.length);
+            OutputStream output = message.body(streamOptions);
+
+            final int chunkSize = 10;
+
+            for (int i = 0; i < buffer.length; i += chunkSize) {
+                output.write(buffer, i, chunkSize);
+            }
+
+            output.close();  // This completes the message send.
+
+            message.tracker().awaitSettlement();
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Receive.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Receive.java
new file mode 100644
index 0000000..c4d24e8
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Receive.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+
+public class Receive {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "send-receive-example";
+        int count = 100;
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            Receiver receiver = connection.openReceiver(address);
+
+            for (int i = 0; i < count; ++i) {
+                Delivery delivery = receiver.receive();
+                Message<String> message = delivery.message();
+
+                System.out.println("Received message with body: " + message.body());
+            }
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Request.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Request.java
new file mode 100644
index 0000000..3e0dc90
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Request.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+
+/**
+ * Sends a Request to a request Queue and awaits a response.
+ */
+public class Request {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "request-respond-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            Receiver dynamicReceiver = connection.openDynamicReceiver();
+            String dynamicAddress = dynamicReceiver.address();
+            System.out.println("Waiting for response to requests on address: " + dynamicAddress);
+
+            SenderOptions senderOptions = new SenderOptions();
+            senderOptions.targetOptions().capabilities("queue");
+
+            Sender requestor = connection.openSender(address, senderOptions);
+            Message<String> request = Message.create("Hello World").replyTo(dynamicAddress);
+            requestor.send(request);
+
+            Delivery response = dynamicReceiver.receive(30, TimeUnit.SECONDS);
+            Message<String> received = response.message();
+            System.out.println("Received message with body: " + received.body());
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Respond.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Respond.java
new file mode 100644
index 0000000..ee09bfd
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Respond.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Sender;
+
+/**
+ * Listens for Requests on a request Queue and sends a response.
+ */
+public class Respond {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "request-respond-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            ReceiverOptions receiverOptions = new ReceiverOptions();
+            receiverOptions.sourceOptions().capabilities("queue");
+
+            Receiver receiver = connection.openReceiver(address, receiverOptions);
+
+            Delivery request = receiver.receive(60, TimeUnit.SECONDS);
+            if (request != null) {
+                Message<String> received = request.message();
+                System.out.println("Received message with body: " + received.body());
+
+                String replyAddress = received.replyTo();
+                if (replyAddress != null) {
+                    Sender sender = connection.openSender(replyAddress);
+                    sender.send(Message.create("Response"));
+                }
+            } else {
+                System.out.println("Failed to read a message during the defined wait interval.");
+            }
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Send.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Send.java
new file mode 100644
index 0000000..5f1f72a
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/Send.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Tracker;
+
+public class Send {
+
+    public static void main(String[] argv) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "send-receive-example";
+        int count = 100;
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            Sender sender = connection.openSender(address);
+
+            for (int i = 0; i < count; ++i) {
+                Message<String> message = Message.create(String.format("Hello World! [%s]", i));
+                Tracker tracker = sender.send(message);
+                tracker.awaitSettlement();
+
+                System.out.println(String.format("Sent message to %s: %s", sender.address(), message.body()));
+            }
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/StreamingFileReceiver.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/StreamingFileReceiver.java
new file mode 100644
index 0000000..4c5586e
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/StreamingFileReceiver.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.StreamDelivery;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.StreamReceiverMessage;
+
+/**
+ * Receives a streamed file and writes it to the path given on the command line.
+ */
+public class StreamingFileReceiver {
+
+    public static void main(String[] args) throws Exception {
+        if (args.length == 0) {
+            System.out.println("Example requires a valid directory where the incoming file should be written");
+            System.exit(1);
+        }
+
+        final File outputPath = new File(args[0]);
+        if (!outputPath.isDirectory() || !outputPath.canWrite()) {
+            System.out.println("Example requires a valid / writable directory to transfer to");
+            System.exit(1);
+        }
+
+        String fileNameKey = "filename";
+        String serverHost = System.getProperty("server_host", "localhost");
+        int serverPort = Integer.getInteger("server_port", 5672);
+        String address = System.getProperty("address", "file-transfer");
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort);
+             StreamReceiver receiver = connection.openStreamReceiver(address)) {
+
+            StreamDelivery delivery = receiver.receive();
+            StreamReceiverMessage message = delivery.message();
+
+            // The remote should have told us the filename of the original file it sent.
+            String filename = (String) message.property(fileNameKey);
+            if (filename == null || filename.isBlank()) {
+                System.out.println("Remote did not include the source filename in the incoming message");
+                System.exit(1);
+            } else {
+                System.out.println("Starting receive of incoming file named: " + filename);
+            }
+
+            try (FileOutputStream outputStream = new FileOutputStream(new File(outputPath, filename))) {
+                message.body().transferTo(outputStream);
+            }
+
+            System.out.println("Received file written to: " + new File(outputPath, filename));
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/StreamingFileSender.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/StreamingFileSender.java
new file mode 100644
index 0000000..e4478b5
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/StreamingFileSender.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderMessage;
+
+/**
+ * Sends the file given in argument zero to the remote address 'file-transfer'
+ */
+public class StreamingFileSender {
+
+    public static void main(String[] args) throws Exception {
+        if (args.length == 0) {
+            System.out.println("Example requires a valid file name to transfer");
+            System.exit(1);
+        }
+
+        final File inputFile = new File(args[0]);
+        if (!inputFile.exists() || !inputFile.canRead()) {
+            System.out.println("Example requires a valid / readable file to transfer");
+            System.exit(1);
+        }
+
+        String fileNameKey = "filename";
+        String serverHost = System.getProperty("server_host", "localhost");
+        int serverPort = Integer.getInteger("server_port", 5672);
+        String address = System.getProperty("address", "file-transfer");
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort);
+             StreamSender sender = connection.openStreamSender(address);
+             FileInputStream inputStream = new FileInputStream(inputFile)) {
+
+            StreamSenderMessage message = sender.beginMessage();
+
+            // Inform the other side what the original file name was.
+            message.property(fileNameKey, inputFile.getName());
+
+            // Creates an OutputStream that writes the file in smaller data sections which allows for
+            // larger file sizes than the single AMQP Data section bounded configuration might allow.
+            // When not specifying a body size the application will need to close the output to indicate
+            // the transfer is complete, here we use a try with resources approach to accomplish that.
+            try (OutputStream output = message.body()) {
+                // Let the streams handle the actual transfer which will block until the full transfer
+                // is complete, or if an error occurs either in the file reader or the stream sender
+                // the message send should be aborted.
+                inputStream.transferTo(output);
+            } catch (IOException ioe) {
+                message.abort();
+            }
+
+            message.tracker().awaitSettlement();
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/TransactedReceiver.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/TransactedReceiver.java
new file mode 100644
index 0000000..f239446
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/TransactedReceiver.java
@@ -0,0 +1,53 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.Session;
+
+public class TransactedReceiver {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "transaction-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver(address);
+
+            session.beginTransaction();
+
+            Delivery delivery = receiver.receive();
+            Message<String> message = delivery.message();
+
+            System.out.println("Received message with body: " + message.body());
+
+            session.commitTransaction();
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/TransactedSender.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/TransactedSender.java
new file mode 100644
index 0000000..366619e
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/TransactedSender.java
@@ -0,0 +1,49 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.client.examples;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Session;
+
+public class TransactedSender {
+
+    public static void main(String[] args) throws Exception {
+        String serverHost = "localhost";
+        int serverPort = 5672;
+        String address = "transaction-example";
+
+        Client client = Client.create();
+
+        try (Connection connection = client.connect(serverHost, serverPort)) {
+            Session session = connection.openSession();
+            Sender sender = connection.openSender(address);
+
+            session.beginTransaction();
+
+            sender.send(Message.create("Transacted Hello"));
+
+            session.commitTransaction();
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/reconnect/ReconnectReceiver.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/reconnect/ReconnectReceiver.java
new file mode 100644
index 0000000..3d3bc3c
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/reconnect/ReconnectReceiver.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples.reconnect;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+
+public class ReconnectReceiver {
+
+    private static final int MESSAGE_COUNT = 10;
+
+    private static String serverHost = "localhost";
+    private static int serverPort = 5672;
+    private static String address = "reconnect-receiver-examples";
+
+    private static String backupServerHost = System.getProperty("backup_server_host");
+    private static int backupServerPort = Integer.getInteger("backup_server_port", 5672);
+
+    public static void main(String[] args) throws Exception {
+        Client client = Client.create();
+        ConnectionOptions connectionOpts = new ConnectionOptions();
+        connectionOpts.reconnectEnabled(true);
+
+        if (backupServerHost != null) {
+            connectionOpts.reconnectOptions().addReconnectHost(backupServerHost, backupServerPort);
+        }
+
+        try (Connection connection = client.connect(serverHost, serverPort, connectionOpts);
+             Receiver receiver = connection.openReceiver(address)) {
+
+            for (int receivedCount = 0; receivedCount < MESSAGE_COUNT; ++receivedCount) {
+                Delivery delivery = receiver.receive();
+                Message<String> message = delivery.message();
+                System.out.print(message.body());
+            }
+        } catch (Exception exp) {
+            System.out.println("Caught exception, exiting.");
+            exp.printStackTrace(System.out);
+            System.exit(1);
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/reconnect/ReconnectSender.java b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/reconnect/ReconnectSender.java
new file mode 100644
index 0000000..3b243bc
--- /dev/null
+++ b/protonj2-client-examples/src/main/java/org/apache/qpid/protonj2/client/examples/reconnect/ReconnectSender.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.examples.reconnect;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Sender;
+
+public class ReconnectSender {
+
+    private static final int MESSAGE_COUNT = 10;
+
+    private static String serverHost = "localhost";
+    private static int serverPort = 5672;
+    private static String address = "reconnect-sender-example";
+
+    private static String backupServerHost = System.getProperty("backup_server_host");
+    private static int backupServerPort = Integer.getInteger("backup_server_port", 5672);
+
+    public static void main(String[] args) throws Exception {
+        Client client = Client.create();
+        ConnectionOptions connectionOpts = new ConnectionOptions();
+        connectionOpts.reconnectEnabled(true);
+
+        if (backupServerHost != null) {
+            connectionOpts.reconnectOptions().addReconnectHost(backupServerHost, backupServerPort);
+        }
+
+        try (Connection connection = client.connect(serverHost, serverPort, connectionOpts);
+             Sender sender = connection.openSender(address)) {
+
+            for (int sentCount = 1; sentCount <= MESSAGE_COUNT; ) {
+                try {
+                    sender.send(Message.create("Hello World: #" + sentCount)).awaitAccepted();
+                    sentCount++;
+                } catch (Exception ex) {
+                    System.out.println("Caught exception during send, will retry on next connect");
+                }
+            }
+        } catch (Exception exp) {
+            System.out.println("Caught exception, exiting.");
+            exp.printStackTrace(System.out);
+            System.exit(1);
+        }
+    }
+}
diff --git a/protonj2-client-examples/src/main/resources/log4j2-test.properties b/protonj2-client-examples/src/main/resources/log4j2-test.properties
new file mode 100644
index 0000000..8dac3e7
--- /dev/null
+++ b/protonj2-client-examples/src/main/resources/log4j2-test.properties
@@ -0,0 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name=ClientModuleTestPropertiesConfig
+status=warn
+
+appender.console.type=Console
+appender.console.name=STDOUT
+appender.console.layout.type=PatternLayout
+appender.console.layout.pattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+
+rootLogger.level=info
+rootLogger.appenderRef.console.ref=STDOUT
+
+logger.client.name=org.apache.qpid.protonj2.client
+logger.client.level=info
+
+logger.proton.name=org.apache.qpid.protonj2
+logger.proton.level=info
+
+
diff --git a/protonj2-client-examples/src/test/resources/log4j2-test.properties b/protonj2-client-examples/src/test/resources/log4j2-test.properties
new file mode 100644
index 0000000..a8cadef
--- /dev/null
+++ b/protonj2-client-examples/src/test/resources/log4j2-test.properties
@@ -0,0 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name=ClientModuleTestPropertiesConfig
+status=warn
+
+appender.console.type=Console
+appender.console.name=STDOUT
+appender.console.layout.type=PatternLayout
+appender.console.layout.pattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+
+rootLogger.level=debug
+rootLogger.appenderRef.console.ref=STDOUT
+
+logger.client.name=org.apache.qpid.protonj2.client
+logger.client.level=debug
+
+logger.proton.name=org.apache.qpid.protonj2
+logger.proton.level=debug
+
+
diff --git a/protonj2-client/README.md b/protonj2-client/README.md
new file mode 100644
index 0000000..42f107f
--- /dev/null
+++ b/protonj2-client/README.md
@@ -0,0 +1,50 @@
+# Qpid protonj2 Client Library
+
+This client provides an imperative API for AMQP messaging applications
+
+Below are some quick pointers you might find useful.
+
+## Using the client library
+
+To use the protonj2 API client library in your projects you can include the maven
+dependency in your project pom file:
+
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2-client</artifactId>
+      <version>${protonj2-version}</version>
+    </dependency>
+
+## Building the code
+
+The project requires Maven 3. Some example commands follow.
+
+Clean previous builds output and install all modules to local repository without
+running the tests:
+
+    mvn clean install -DskipTests
+
+Install all modules to the local repository after running all the tests:
+
+    mvn clean install
+
+Perform a subset tests on the packaged release artifacts without
+installing:
+
+    mvn clean verify -Dtest=TestNamePattern*
+
+Execute the tests and produce code coverage report:
+
+    mvn clean test jacoco:report
+
+## Examples
+
+First build and install all the modules as detailed above (if running against
+a source checkout/release, rather than against released binaries) and then
+consult the README in the protonj2-client-examples module itself.
+
+## Documentation
+
+There is some basic documentation in the protonj2-client-docs module.
+
+
diff --git a/protonj2-client/pom.xml b/protonj2-client/pom.xml
new file mode 100644
index 0000000..33dfacb
--- /dev/null
+++ b/protonj2-client/pom.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<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/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>protonj2-client</artifactId>
+  <name>Qpid protonj2 Client Library</name>
+  <description>Imperative API for AMQP Messaging.</description>
+  <packaging>jar</packaging>
+
+  <properties>
+    <!-- See also maven.compiler.release in the java9on profile -->
+    <maven.compiler.source>11</maven.compiler.source>
+    <maven.compiler.target>11</maven.compiler.target>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-buffer</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-handler</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport-native-epoll</artifactId>
+      <classifier>${netty-transport-native-epoll-classifier}</classifier>
+    </dependency>
+    <dependency>
+      <groupId>io.netty.incubator</groupId>
+      <artifactId>netty-incubator-transport-native-io_uring</artifactId>
+      <classifier>${netty-transport-native-io-uring-classifier}</classifier>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport-native-kqueue</artifactId>
+      <classifier>${netty-transport-native-kqueue-classifier}</classifier>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-codec-http</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2-test-driver</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-tcnative-boringssl-static</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>${project.basedir}/src/main/resources</directory>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>${project.basedir}/src/main/filtered-resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+    </resources>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Automatic-Module-Name>org.apache.qpid.protonj2.client</Automatic-Module-Name>
+            </manifestEntries>
+            <manifest>
+              <addDefaultSpecificationEntries>false</addDefaultSpecificationEntries>
+              <addDefaultImplementationEntries>false</addDefaultImplementationEntries>
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/AdvancedMessage.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/AdvancedMessage.java
new file mode 100644
index 0000000..78b9080
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/AdvancedMessage.java
@@ -0,0 +1,281 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.impl.ClientMessage;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+
+/**
+ * Advanced AMQP Message object that provides a thin abstraction to raw AMQP types
+ *
+ * @param <E> The type of the message body that this message carries
+ */
+public interface AdvancedMessage<E> extends Message<E> {
+
+    /**
+     * Creates a new {@link AdvancedMessage} instance using the library default implementation.
+     *
+     * @param <V> The type to use when specifying the body section value type.
+     *
+     * @return a new {@link AdvancedMessage} instance.
+     */
+    static <V> AdvancedMessage<V> create() {
+        return ClientMessage.createAdvancedMessage();
+    }
+
+    /**
+     * Return the current {@link Header} assigned to this message, if none was assigned yet
+     * then this method returns <code>null</code>.
+     *
+     * @return the currently assigned {@link Header} for this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    Header header() throws ClientException;
+
+    /**
+     * Assign or replace the {@link Header} instance associated with this message.
+     *
+     * @param header
+     *      The {@link Header} value to assign to this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing the message {@link Header} value.
+     */
+    AdvancedMessage<E> header(Header header) throws ClientException;
+
+    /**
+     * Return the current {@link MessageAnnotations} assigned to this message, if none was assigned yet
+     * then this method returns <code>null</code>.
+     *
+     * @return the currently assigned {@link MessageAnnotations} for this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    MessageAnnotations annotations() throws ClientException;
+
+    /**
+     * Assign or replace the {@link MessageAnnotations} instance associated with this message.
+     *
+     * @param messageAnnotations
+     *      The {@link MessageAnnotations} value to assign to this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing the message {@link MessageAnnotations} value.
+     */
+    AdvancedMessage<E> annotations(MessageAnnotations messageAnnotations) throws ClientException;
+
+    /**
+     * Return the current {@link Properties} assigned to this message, if none was assigned yet
+     * then this method returns <code>null</code>.
+     *
+     * @return the currently assigned {@link Properties} for this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    Properties properties() throws ClientException;
+
+    /**
+     * Assign or replace the {@link Properties} instance associated with this message.
+     *
+     * @param properties
+     *      The {@link Properties} value to assign to this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing the message {@link Properties} value.
+     */
+    AdvancedMessage<E> properties(Properties properties) throws ClientException;
+
+    /**
+     * Return the current {@link ApplicationProperties} assigned to this message, if none was assigned yet
+     * then this method returns <code>null</code>.
+     *
+     * @return the currently assigned {@link ApplicationProperties} for this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    ApplicationProperties applicationProperties() throws ClientException;
+
+    /**
+     * Assign or replace the {@link ApplicationProperties} instance associated with this message.
+     *
+     * @param applicationProperties
+     *      The {@link ApplicationProperties} value to assign to this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing the message {@link ApplicationProperties} value.
+     */
+    AdvancedMessage<E> applicationProperties(ApplicationProperties applicationProperties) throws ClientException;
+
+    /**
+     * Return the current {@link Footer} assigned to this message, if none was assigned yet
+     * then this method returns <code>null</code>.
+     *
+     * @return the currently assigned {@link Footer} for this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    Footer footer() throws ClientException;
+
+    /**
+     * Assign or replace the {@link Footer} instance associated with this message.
+     *
+     * @param footer
+     *      The {@link Footer} value to assign to this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing the message {@link Footer} value.
+     */
+    AdvancedMessage<E> footer(Footer footer) throws ClientException;
+
+    /**
+     * @return the currently assigned message format for this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    int messageFormat() throws ClientException;
+
+    /**
+     * Sets the message format to use when the message is sent.  The exact structure of a
+     * message, together with its encoding, is defined by the message format (default is
+     * the AMQP defined message format zero.
+     * <p>
+     * This field MUST be specified for the first transfer of a multi-transfer message, if
+     * it is not set at the time of send of the first transfer the sender uses the AMQP
+     * default value of zero for this field.
+     * <p>
+     * The upper three octets of a message format code identify a particular message format.
+     * The lowest octet indicates the version of said message format. Any given version of
+     * a format is forwards compatible with all higher versions.
+     * <pre>
+     *
+     *       3 octets      1 octet
+     *    +----------------+---------+
+     *    | message format | version |
+     *    +----------------+---------+
+     *    |                          |
+     *   msb                        lsb
+     *
+     * </pre>
+     *
+     * @param messageFormat
+     *      The message format to encode into the transfer frame that carries the message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while configuring the message format.
+     */
+    AdvancedMessage<E> messageFormat(int messageFormat) throws ClientException;
+
+    /**
+     * Adds the given {@link Section} to the internal collection of sections that will be sent
+     * to the remote peer when this message is encoded.  If a previous section was added by a call
+     * to the {@link Message#body(Object)} method it should be retained as the first element of
+     * the running list of body sections contained in this message.
+     * <p>
+     * The implementation should make an attempt to validate that sections added are valid for
+     * the message format that is assigned when they are added.
+     *
+     * @param bodySection
+     *      The {@link Section} instance to append to the internal collection.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing to the message body sections.
+     */
+    AdvancedMessage<E> addBodySection(Section<?> bodySection) throws ClientException;
+
+    /**
+     * Sets the body {@link Section} instances to use when encoding this message.  The value
+     * given replaces any existing sections assigned to this message through the {@link Message#body(Object)}
+     * or {@link AdvancedMessage#addBodySection(Section)} methods.  Calling this method with a null
+     * or empty collection is equivalent to calling the {@link #clearBodySections()} method.
+     *
+     * @param sections
+     *      The {@link Collection} of {@link Section} instance to assign this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while writing to the message body sections.
+     */
+    AdvancedMessage<E> bodySections(Collection<Section<?>> sections) throws ClientException;
+
+    /**
+     * Create and return a {@link Collection} that contains the {@link Section} instances currently
+     * assigned to this message.  Changes to the returned Collection are not reflected in the Message.
+     *
+     * @return a {@link Collection} that is a copy of the current sections assigned to this message.
+     *
+     * @throws ClientException if an error occurs while retrieving the message data.
+     */
+    Collection<Section<?>> bodySections() throws ClientException;
+
+    /**
+     * Performs the given action for each body {@link Section} of the {@link AdvancedMessage} until all
+     * sections have been presented to the given {@link Consumer} or the consumer throws an exception.
+     *
+     * @param consumer
+     *      the {@link Consumer} that will operate on each of the body sections in this message.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while iterating over the message data.
+     */
+    AdvancedMessage<E> forEachBodySection(Consumer<Section<?>> consumer) throws ClientException;
+
+    /**
+     * Clears all current body {@link Section} elements from the {@link AdvancedMessage}.
+     *
+     * @return this {@link AdvancedMessage} instance.
+     *
+     * @throws ClientException if an error occurs while clearing the message body sections.
+     */
+    AdvancedMessage<E> clearBodySections() throws ClientException;
+
+    /**
+     * Encodes the {@link AdvancedMessage} for transmission by the client.  The provided {@link DeliveryAnnotations}
+     * can be included or augmented by the {@link AdvancedMessage} implementation based on the target message format.
+     * The implementation is responsible for ensuring that the delivery annotations are treated correctly encoded into
+     * the correct location in the message.
+     *
+     * @param deliveryAnnotations
+     *      A {@link Map} of delivery annotation values that were requested to be included in the transmitted message.
+     *
+     * @return the encoded form of this message in a {@link ProtonBuffer} instance.
+     *
+     * @throws ClientException if an error occurs while encoding the message data.
+     */
+    ProtonBuffer encode(Map<String, Object> deliveryAnnotations) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Client.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Client.java
new file mode 100644
index 0000000..2456ea5
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Client.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.concurrent.Future;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.impl.ClientInstance;
+
+/**
+ * The Container that hosts AMQP Connections
+ */
+public interface Client extends AutoCloseable {
+
+    /**
+     * @return a new {@link Client} instance configured with defaults.
+     */
+    static Client create() {
+        return ClientInstance.create();
+    }
+
+    /**
+     * Create a new {@link Client} instance using provided configuration options.
+     *
+     * @param options
+     * 		The configuration options to use when creating the client.
+     *
+     * @return a new {@link Client} instance configured using the provided options.
+     */
+    static Client create(ClientOptions options) {
+        return ClientInstance.create(options);
+    }
+
+    /**
+     * @return the container id assigned to this {@link Client} instance.
+     */
+    String containerId();
+
+    /**
+     * Closes all currently open {@link Connection} instances created by this client.
+     * <p>
+     * This method blocks and waits for each connection to close in turn using the configured
+     * close timeout of the {@link ConnectionOptions} that the connection was created with.
+     */
+    @Override
+    void close();
+
+    /**
+     * Closes all currently open {@link Connection} instances created by this client.
+     * <p>
+     * This method blocks and waits for each connection to be closed in turn using the configured
+     * close timeout of the {@link ConnectionOptions} that the connection was created with.
+     *
+     * @return a {@link Future} that will be completed when all open connections have closed.
+     */
+    Future<Client> closeAsync();
+
+    /**
+     * Connect to the specified host and port, without credentials and with all
+     * connection options set to their defaults.
+     *
+     * @param host
+     *            the host to connect to
+     * @param port
+     *            the port to connect to
+     *
+     * @return connection, establishment not yet completed
+     *
+     * @throws ClientException if the {@link Client} is closed or an error occurs during connect.
+     */
+    Connection connect(String host, int port) throws ClientException;
+
+    /**
+     * Connect to the specified host and port, with given connection options.
+     *
+     * @param host
+     *            the host to connect to
+     * @param port
+     *            the port to connect to
+     * @param options
+     *            options to use when creating the connection.
+     *
+     * @return connection, establishment not yet completed
+     *
+     * @throws ClientException if the {@link Client} is closed or an error occurs during connect.
+     */
+    Connection connect(String host, int port, ConnectionOptions options) throws ClientException;
+
+    /**
+     * Connect to the specified host, using the default port, without credentials and with all
+     * connection options set to their defaults.
+     *
+     * @param host
+     *            the host to connect to
+     *
+     * @return connection, establishment not yet completed
+     *
+     * @throws ClientException if the {@link Client} is closed or an error occurs during connect.
+     */
+    Connection connect(String host) throws ClientException;
+
+    /**
+     * Connect to the specified host, using the default port, without credentials and with all
+     * connection options set to their defaults.
+     *
+     * @param host
+     *            the host to connect to
+     * @param options
+     *            options to use when creating the connection.
+     *
+     * @return connection, establishment not yet completed
+     *
+     * @throws ClientException if the {@link Client} is closed or an error occurs during connect.
+     */
+    Connection connect(String host, ConnectionOptions options) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ClientOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ClientOptions.java
new file mode 100644
index 0000000..d9a2435
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ClientOptions.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * Container Options for customizing the behavior of the Container
+ */
+public class ClientOptions {
+
+    private String id;
+    private String futureType;
+
+    public ClientOptions() {}
+
+    public ClientOptions(ClientOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    /**
+     * @return the ID configured the Container
+     */
+    public String id() {
+        return id;
+    }
+
+    /**
+     * Sets the container ID that should be used when creating Connections
+     *
+     * @param id
+     *      The container Id that should be assigned to container connections.
+     *
+     * @return this options class for chaining.
+     */
+    public ClientOptions id(String id) {
+        this.id = id;
+        return this;
+    }
+
+    /**
+     * @return the configure future type to use for this client connection
+     */
+    public String futureType() {
+        return futureType;
+    }
+
+    /**
+     * Sets the desired future type that the client connection should use when creating
+     * the futures used by the API.  By default the client will select a Future implementation
+     * by itself however the user can override this selection here if desired.
+     *
+     * @param futureType
+     *      The name of the future type to use.
+     *
+     * @return this options object for chaining.
+     */
+    public ClientOptions futureType(String futureType) {
+        this.futureType = futureType;
+        return this;
+    }
+
+    @Override
+    public ClientOptions clone() {
+        return copyInto(new ClientOptions());
+    }
+
+    /**
+     * Copy all options from this {@link ClientOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this options class for chaining.
+     */
+    public ClientOptions copyInto(ClientOptions other) {
+        other.id(id);
+        other.futureType(futureType);
+
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Connection.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Connection.java
new file mode 100644
index 0000000..c129982
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Connection.java
@@ -0,0 +1,483 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+
+/**
+ * Top level {@link Connection} object that can be used as a stand alone API for sending
+ * messages and creating {@link Receiver} instances for message consumption. The Connection
+ * API also exposes a {@link Session} based API for more advanced messaging use cases.
+ *
+ * When a Connection is closed all the resources created by the connection are implicitly closed.
+ */
+public interface Connection extends AutoCloseable {
+
+    /**
+     * @return the {@link Client} instance that holds this {@link Connection}
+     */
+    Client client();
+
+    /**
+     * When a {@link Connection} is created it may not be opened on the remote peer, the future returned
+     * from this method allows the caller to await the completion of the Connection open by the remote before
+     * proceeding on to other messaging operations.  If the open of the connection fails at the remote an
+     * {@link Exception} is thrown from the {@link Future#get()} method when called.
+     *
+     * @return a {@link Future} that will be completed when the remote opens this {@link Connection}.
+     */
+    Future<Connection> openFuture();
+
+    /**
+     * Requests a close of the {@link Connection} at the remote and waits until the Connection has been
+     * fully closed or until the configured {@link ConnectionOptions#closeTimeout()} is exceeded.
+     */
+    @Override
+    void close();
+
+    /**
+     * Requests a close of the {@link Connection} at the remote and waits until the Connection has been
+     * fully closed or until the configured {@link ConnectionOptions#closeTimeout()} is exceeded.
+     *
+     * @param error
+     *      The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     */
+    void close(ErrorCondition error);
+
+    /**
+     * Requests a close of the {@link Connection} at the remote and returns a {@link Future} that will be
+     * completed once the Connection has been fully closed.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Connection}.
+     */
+    Future<Connection> closeAsync();
+
+    /**
+     * Requests a close of the {@link Connection} at the remote and returns a {@link Future} that will be
+     * completed once the Connection has been fully closed.
+     *
+     * @param error
+     * 		The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Connection}.
+     */
+    Future<Connection> closeAsync(ErrorCondition error);
+
+    /**
+     * Creates a receiver used to consumer messages from the given node address.  The returned receiver will
+     * be configured using default options and will take its timeout configuration values from those specified
+     * in the parent {@link Connection}.
+     *
+     * The returned receiver may not have been opened on the remote when it is returned.  Some methods of the
+     * {@link Receiver} can block until the remote fully opens the receiver, the user can wait for the remote
+     * to respond to the open request by calling the {@link Receiver#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     *
+     * @return the consumer.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openReceiver(String address) throws ClientException;
+
+    /**
+     * Creates a receiver used to consumer messages from the given node address.  The returned receiver
+     * will be configured using the options provided in the given {@link ReceiverOptions} instance.
+     *
+     * The returned receiver may not have been opened on the remote when it is returned.  Some methods of the
+     * {@link Receiver} can block until the remote fully opens the receiver, the user can wait for the remote
+     * to respond to the open request by calling the {@link Receiver#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     * @param receiverOptions
+     *            The options for this receiver.
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openReceiver(String address, ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a receiver used to consume messages from the given node address and configure it
+     * such that the remote create a durable node.
+     *
+     * @param address
+     * 			The source address to attach the consumer to.
+     * @param subscriptionName
+     * 			The name to give the subscription (link name).
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDurableReceiver(String address, String subscriptionName) throws ClientException;
+
+    /**
+     * Creates a receiver used to consume messages from the given node address and configure it
+     * such that the remote create a durable node.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     * @param subscriptionName
+     * 			The name to give the subscription (link name).
+     * @param receiverOptions
+     *            The options for this receiver.
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDurableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.  The returned receiver
+     * will be configured using default options and will take its timeout configuration values from those
+     * specified in the parent {@link Connection}.
+     *
+     * The returned receiver may not have been opened on the remote when it is returned.  Some methods of the
+     * {@link Receiver} can block until the remote fully opens the receiver, the user can wait for the remote
+     * to respond to the open request by calling the {@link Receiver#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver() throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * The returned receiver may not have been opened on the remote when it is returned.  Some methods of the
+     * {@link Receiver} can block until the remote fully opens the receiver, the user can wait for the remote
+     * to respond to the open request by calling the {@link Receiver#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param dynamicNodeProperties
+     * 		The dynamic node properties to be applied to the node created by the remote.
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties) throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * The returned receiver may not have been opened on the remote when it is returned.  Some methods of the
+     * {@link Receiver} can block until the remote fully opens the receiver, the user can wait for the remote
+     * to respond to the open request by calling the {@link Receiver#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param receiverOptions
+     * 		The options for this receiver.
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver(ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * The returned receiver may not have been opened on the remote when it is returned.  Some methods of the
+     * {@link Receiver} can block until the remote fully opens the receiver, the user can wait for the remote
+     * to respond to the open request by calling the {@link Receiver#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param dynamicNodeProperties
+     * 		The dynamic node properties to be applied to the node created by the remote.
+     * @param receiverOptions
+     *      The options for this receiver.
+     *
+     * @return the newly created {@link Receiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a streaming message receiver used to consume large messages from the given node address.  The
+     * returned {@link StreamReceiver} will be configured using default options and will take its timeout
+     * configuration values from those specified in the parent {@link Connection}.
+     *
+     * The returned stream receiver may not have been opened on the remote when it is returned.  Some methods of
+     * the {@link StreamReceiver} can block until the remote fully opens the receiver link, the user can wait for
+     * the remote to respond to the open request by calling the {@link StreamReceiver#openFuture()} method and using
+     * the {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     *
+     * @return the newly created {@link StreamReceiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    StreamReceiver openStreamReceiver(String address) throws ClientException;
+
+    /**
+     * Creates a streaming message receiver used to consume large messages from the given node address.  The
+     * returned receiver will be configured using the options provided in the given {@link ReceiverOptions}
+     * instance.
+     *
+     * The returned {@link StreamReceiver} may not have been opened on the remote when it is returned.  Some
+     * methods of the {@link StreamReceiver} can block until the remote fully opens the receiver link, the user
+     * can wait for the remote to respond to the open request by calling the {@link StreamReceiver#openFuture()}
+     * method and using the {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     * @param receiverOptions
+     *            The options for this receiver.
+     *
+     * @return the newly created {@link StreamReceiver} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    StreamReceiver openStreamReceiver(String address, StreamReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Returns the default anonymous sender used by this {@link Connection} for {@link #send(Message)}
+     * calls.  If the sender has not been created yet this call will initiate its creation and open with
+     * the remote peer.
+     *
+     * @return the sender.
+     *
+     * @throws ClientException if an internal error occurs opening the default sender.
+     * @throws ClientUnsupportedOperationException if the remote did not signal support for anonymous relays.
+     */
+    Sender defaultSender() throws ClientException;
+
+    /**
+     * Creates a sender used to send messages to the given node address.  The returned sender will
+     * be configured using default options and will take its timeout configuration values from those
+     * specified in the parent {@link Connection}.
+     *
+     * The returned {@link Sender} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link Sender} can block until the remote fully opens the sender, the user can wait for the
+     * remote to respond to the open request by calling the {@link Sender#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The target address to attach to, cannot be null.
+     *
+     * @return the sender.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Sender openSender(String address) throws ClientException;
+
+    /**
+     * Creates a sender used to send messages to the given node address.
+     *
+     * The returned {@link Sender} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link Sender} can block until the remote fully opens the sender, the user can wait for the
+     * remote to respond to the open request by calling the {@link Sender#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The target address to attach to, cannot be null.
+     * @param senderOptions
+     *            The options for this sender.
+     *
+     * @return the sender.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Sender openSender(String address, SenderOptions senderOptions) throws ClientException;
+
+    /**
+     * Creates a stream sender used to send large messages to the given node address.  The returned sender will
+     * be configured using default options and will take its timeout configuration values from those
+     * specified in the parent {@link Connection}.
+     *
+     * The returned {@link StreamSender} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link StreamSender} can block until the remote fully opens the sender, the user can wait for the
+     * remote to respond to the open request by calling the {@link StreamSender#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The target address to attach to, cannot be null.
+     *
+     * @return the stream sender.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    StreamSender openStreamSender(String address) throws ClientException;
+
+    /**
+     * Creates a streaming sender used to send large messages to the given node address.
+     * <p>
+     * The returned {@link StreamSender} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link StreamSender} can block until the remote fully opens the sender, the user can wait for the
+     * remote to respond to the open request by calling the {@link StreamSender#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param address
+     *            The target address to attach to, cannot be null.
+     * @param senderOptions
+     *            The options for this sender.
+     *
+     * @return the sender.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    StreamSender openStreamSender(String address, StreamSenderOptions senderOptions) throws ClientException;
+
+    /**
+     * Creates a sender that is established to the 'anonymous relay' and as such each message
+     * that is sent using this sender must specify an address in its destination address field.
+     * The returned sender will be configured using default options and will take its timeout
+     * configuration values from those specified in the parent {@link Connection}.
+     *
+     * The returned {@link Sender} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link Sender} can block until the remote fully opens the sender, the user can wait for the
+     * remote to respond to the open request by calling the {@link Sender#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @return the sender.
+     *
+     * @throws ClientException if an internal error occurs.
+     * @throws ClientUnsupportedOperationException if the remote did not signal support for anonymous relays.
+     */
+    Sender openAnonymousSender() throws ClientException;
+
+    /**
+     * Creates a sender that is established to the 'anonymous relay' and as such each
+     * message that is sent using this sender must specify an address in its destination
+     * address field.
+     *
+     * The returned {@link Sender} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link Sender} can block until the remote fully opens the sender, the user can wait for the
+     * remote to respond to the open request by calling the {@link Sender#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param senderOptions
+     *            The options for this sender.
+     *
+     * @return the sender.
+     *
+     * @throws ClientException if an internal error occurs.
+     * @throws ClientUnsupportedOperationException if the remote did not signal support for anonymous relays.
+     */
+    Sender openAnonymousSender(SenderOptions senderOptions) throws ClientException;
+
+    /**
+     * Returns the default {@link Session} instance that is used by this Connection to
+     * create the default anonymous connection {@link Sender} as well as creating those
+     * resources created from the {@link Connection} such as {@link Sender} and {@link Receiver}
+     * instances not married to a specific {@link Session}.
+     *
+     * @return a new {@link Session} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Session defaultSession() throws ClientException;
+
+    /**
+     * Creates a new {@link Session} instance for use by the client application.  The returned session
+     * will be configured using default options and will take its timeout configuration values from those
+     * specified in the parent {@link Connection}.
+     *
+     * The returned {@link Session} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link Session} can block until the remote fully opens the session, the user can wait for the
+     * remote to respond to the open request by calling the {@link Session#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @return a new {@link Session} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Session openSession() throws ClientException;
+
+    /**
+     * Creates a new {@link Session} instance for use by the client application.
+     *
+     * The returned {@link Session} may not have been opened on the remote when it is returned.  Some methods
+     * of the {@link Session} can block until the remote fully opens the session, the user can wait for the
+     * remote to respond to the open request by calling the {@link Session#openFuture()} method and using the
+     * {@link Future#get()} methods to wait for completion.
+     *
+     * @param options
+     *      The {@link SessionOptions} that control properties of the created session.
+     *
+     * @return a new {@link Session} instance.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Session openSession(SessionOptions options) throws ClientException;
+
+    /**
+     * Sends the given {@link Message} using the internal connection sender.
+     * <p>
+     * The connection {@link Sender} is an anonymous AMQP sender which requires that the
+     * given message has a valid to value set.
+     *
+     * @param message
+     * 		The message to send
+     *
+     * @return a {@link Tracker} that allows the client to track settlement of the message.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Tracker send(Message<?> message) throws ClientException;
+
+    /**
+     * Returns the properties that the remote provided upon successfully opening the {@link Connection}.  If the
+     * open has not completed yet this method will block to await the open response which carries the remote
+     * properties.  If the remote provides no properties this method will return null.
+     *
+     * @return any properties provided from the remote once the connection has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Connection} remote properties.
+     */
+    Map<String, Object> properties() throws ClientException;
+
+    /**
+     * Returns the offered capabilities that the remote provided upon successfully opening the {@link Connection}.
+     * If the open has not completed yet this method will block to await the open response which carries the
+     * remote offered capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any capabilities provided from the remote once the connection has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Connection} remote offered capabilities.
+     */
+    String[] offeredCapabilities() throws ClientException;
+
+    /**
+     * Returns the desired capabilities that the remote provided upon successfully opening the {@link Connection}.
+     * If the open has not completed yet this method will block to await the open response which carries the
+     * remote desired capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any desired capabilities provided from the remote once the connection has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Connection} remote desired capabilities.
+     */
+    String[] desiredCapabilities() throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ConnectionEvent.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ConnectionEvent.java
new file mode 100644
index 0000000..7989dbd
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ConnectionEvent.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * An event object that accompanies events fired to handlers configured in the
+ * {@link ConnectionOptions} which are signaled during specific {@link Connection}
+ * event points.
+ */
+public class ConnectionEvent {
+
+    private final String host;
+    private final int port;
+
+    /**
+     * Creates the event object with all immutable data provided.
+     *
+     * @param host
+     *      the host that is associated with this {@link ConnectionEvent}
+     * @param port
+     *      the port that is associated with this {@link ConnectionEvent}
+     */
+    public ConnectionEvent(String host, int port) {
+        this.host = host;
+        this.port = port;
+    }
+
+    /**
+     * Gets the host that is associated with this event which for a successful
+     * {@link Connection} event would be the currently active host and for an
+     * interrupted or failed Connection this host would indicate the host where
+     * the {@link Connection} had previously been established.
+     *
+     * @return the host that is associated with this {@link ConnectionEvent}
+     */
+    public String host() {
+        return host;
+    }
+
+    /**
+     * Gets the port that is associated with this event which for a successful
+     * {@link Connection} event would be the currently active port and for an
+     * interrupted or failed Connection this port would indicate the host where
+     * the {@link Connection} had previously been established.
+     *
+     * @return the port that is associated with this {@link ConnectionEvent}
+     */
+    public int port() {
+        return port;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ConnectionOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ConnectionOptions.java
new file mode 100644
index 0000000..374de8d
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ConnectionOptions.java
@@ -0,0 +1,650 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientSendTimedOutException;
+import org.apache.qpid.protonj2.types.transport.Open;
+
+/**
+ * Options that control the behaviour of the {@link Connection} created from them.
+ */
+public class ConnectionOptions {
+
+    public static final String[] DEFAULT_DESIRED_CAPABILITIES = new String[] { "ANONYMOUS-RELAY" };
+
+    public static final long INFINITE = -1;
+    public static final long DEFAULT_OPEN_TIMEOUT = 15000;
+    public static final long DEFAULT_CLOSE_TIMEOUT = 60000;
+    public static final long DEFAULT_SEND_TIMEOUT = INFINITE;
+    public static final long DEFAULT_REQUEST_TIMEOUT = INFINITE;
+    public static final long DEFAULT_IDLE_TIMEOUT = 60000;
+    public static final long DEFAULT_DRAIN_TIMEOUT = 60000;
+    public static final int DEFAULT_CHANNEL_MAX = 65535;
+    public static final int DEFAULT_MAX_FRAME_SIZE = 65536;
+
+    private long sendTimeout = DEFAULT_SEND_TIMEOUT;
+    private long requestTimeout = DEFAULT_REQUEST_TIMEOUT;
+    private long openTimeout = DEFAULT_OPEN_TIMEOUT;
+    private long closeTimeout = DEFAULT_CLOSE_TIMEOUT;
+    private long idleTimeout = DEFAULT_IDLE_TIMEOUT;
+    private long drainTimeout = DEFAULT_DRAIN_TIMEOUT;
+
+    private final TransportOptions transport = new TransportOptions();
+    private final ReconnectOptions reconnect = new ReconnectOptions();
+    private final SslOptions ssl = new SslOptions();
+    private final SaslOptions sasl = new SaslOptions();
+
+    private String user;
+    private String password;
+    private int channelMax = DEFAULT_CHANNEL_MAX;
+    private int maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
+    private String[] offeredCapabilities;
+    private String[] desiredCapabilities = DEFAULT_DESIRED_CAPABILITIES;
+    private Map<String, Object> properties;
+    private String virtualHost;
+    private boolean traceFrames;
+
+    private BiConsumer<Connection, ConnectionEvent> connectedhedHandler;
+    private BiConsumer<Connection, DisconnectionEvent> disconnectedHandler;
+    private BiConsumer<Connection, DisconnectionEvent> interruptedHandler;
+    private BiConsumer<Connection, ConnectionEvent> reconnectedHandler;
+
+    /**
+     * Create a new {@link ConnectionOptions} instance configured with default configuration settings.
+     */
+    public ConnectionOptions() {
+        // Defaults
+    }
+
+    /**
+     * Creates a {@link ConnectionOptions} instance that is a copy of the given instance.
+     *
+     * @param options
+     *      The {@link ConnectionOptions} instance whose configuration should be copied to this one.
+     */
+    public ConnectionOptions(ConnectionOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    @Override
+    public ConnectionOptions clone() {
+        return copyInto(new ConnectionOptions());
+    }
+
+    /**
+     * Copy all options from this {@link ConnectionOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    protected ConnectionOptions copyInto(ConnectionOptions other) {
+        other.closeTimeout(closeTimeout);
+        other.openTimeout(openTimeout);
+        other.sendTimeout(sendTimeout);
+        other.requestTimeout(requestTimeout);
+        other.idleTimeout(idleTimeout);
+        other.drainTimeout(drainTimeout);
+        other.channelMax(channelMax);
+        other.maxFrameSize(maxFrameSize);
+        other.user(user);
+        other.password(password);
+        other.traceFrames(traceFrames);
+        other.connectedHandler(connectedhedHandler);
+        other.interruptedHandler(interruptedHandler);
+        other.reconnectedHandler(reconnectedHandler);
+        other.disconnectedHandler(disconnectedHandler);
+
+        if (offeredCapabilities != null) {
+            other.offeredCapabilities(Arrays.copyOf(offeredCapabilities, offeredCapabilities.length));
+        }
+        if (desiredCapabilities != null) {
+            other.desiredCapabilities(Arrays.copyOf(desiredCapabilities, desiredCapabilities.length));
+        }
+        if (properties != null) {
+            other.properties(new HashMap<>(properties));
+        }
+
+        transport.copyInto(other.transportOptions());
+        ssl.copyInto(other.sslOptions());
+        sasl.copyInto(other.saslOptions());
+        reconnect.copyInto(other.reconnectOptions());
+
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is closed.
+     */
+    public long closeTimeout() {
+        return closeTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to close
+     * a resource such as a {@link Connection}, {@link Session}, {@link Sender} or {@link Receiver} h
+     * as been honored.
+     *
+     * @param closeTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions closeTimeout(long closeTimeout) {
+        this.closeTimeout = closeTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is opened.
+     */
+    public long openTimeout() {
+        return openTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to open
+     * a resource such as a {@link Connection}, {@link Session}, {@link Sender} or {@link Receiver}
+     * has been honored.
+     *
+     * @param openTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions openTimeout(long openTimeout) {
+        this.openTimeout = openTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is message send.
+     */
+    public long sendTimeout() {
+        return sendTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a send operation to complete.  A send will block if the
+     * remote has not granted the {@link Sender} or the {@link Session} credit to do so, if the send blocks
+     * for longer than this timeout the send call will fail with an {@link ClientSendTimedOutException}
+     * exception to indicate that the send did not complete.
+     *
+     * @param sendTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions sendTimeout(long sendTimeout) {
+        this.sendTimeout = sendTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource makes a request.
+     */
+    public long requestTimeout() {
+        return requestTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to
+     * perform some action such as starting a new transaction.  If the remote does not respond
+     * within the configured timeout the resource making the request will mark it as failed and
+     * return an error to the request initiator usually in the form of a
+     * {@link ClientOperationTimedOutException}.
+     *
+     * @param requestTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions requestTimeout(long requestTimeout) {
+        this.requestTimeout = requestTimeout;
+        return this;
+    }
+
+    /**
+     * @return the configured or default channel max value for create {@link Connection} instances.
+     */
+    public int channelMax() {
+        return channelMax;
+    }
+
+    /**
+     * Configure the channel maximum value for the new {@link Connection} created with these options.
+     * <p>
+     * The channel max value controls how many {@link Session} instances can be created by a given
+     * Connection, the default value is <i>65535</i>.
+     *
+     * @param channelMax
+     *      The channel max value to assign to newly created {@link Connection} instances.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions channelMax(int channelMax) {
+        if (channelMax < 0 || channelMax > 65535) {
+            throw new IllegalArgumentException("Cannot set a channel max less than zero or greater than 65535");
+        }
+
+        this.channelMax = channelMax;
+        return this;
+    }
+
+    /**
+     * @return the configure maximum frame size value for newly create {@link Connection} instances.
+     */
+    public int maxFrameSize() {
+        return maxFrameSize;
+    }
+
+    /**
+     * Sets the max frame size (in bytes), values of -1 indicates to use the client selected default.
+     *
+     * @param maxFrameSize the frame size in bytes.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions maxFrameSize(int maxFrameSize) {
+        this.maxFrameSize = maxFrameSize;
+        return this;
+    }
+
+    /**
+     * @return the configured idle timeout value that will be sent to the remote.
+     */
+    public long idleTimeout() {
+        return idleTimeout;
+    }
+
+    /**
+     * Sets the idle timeout (in milliseconds) after which the connection will
+     * be closed if the peer has not send any data. The provided value will be
+     * halved before being transmitted as our advertised idle-timeout in the
+     * AMQP {@link Open} frame.
+     *
+     * @param idleTimeout the timeout in milliseconds.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions idleTimeout(long idleTimeout) {
+        this.idleTimeout = idleTimeout;
+        return this;
+    }
+
+    /**
+     * @return the configured drain timeout value that will use to fail a pending drain request.
+     */
+    public long drainTimeout() {
+        return drainTimeout;
+    }
+
+    /**
+     * Sets the drain timeout (in milliseconds) after which a {@link Receiver} request to drain
+     * link credit is considered failed and the request will be marked as such.
+     *
+     * @param drainTimeout
+     *      the drainTimeout to use for receiver links.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions drainTimeout(long drainTimeout) {
+        this.drainTimeout = drainTimeout;
+        return this;
+    }
+
+    /**
+     * @return the offeredCapabilities that have been configured.
+     */
+    public String[] offeredCapabilities() {
+        return offeredCapabilities;
+    }
+
+    /**
+     * Sets the collection of capabilities to offer to the remote from a new {@link Connection}
+     * created using these {@link ConnectionOptions}.  The offered capabilities advertise to the
+     * remote capabilities that this {@link Connection} supports.
+     *
+     * @param offeredCapabilities
+     *      the offeredCapabilities to set on a new {@link Connection}.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions offeredCapabilities(String... offeredCapabilities) {
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the desiredCapabilities that have been configured.
+     */
+    public String[] desiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    /**
+     * Sets the collection of capabilities to request from the remote for a new {@link Connection}
+     * created using these {@link ConnectionOptions}.  The desired capabilities inform the remote
+     * peer of the various capabilities the new {@link Connection} requires and the remote should
+     * return those that it supports in its offered capabilities.
+     *
+     * @param desiredCapabilities
+     *      the desiredCapabilities to set on a new {@link Connection}.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions desiredCapabilities(String... desiredCapabilities) {
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the properties that have been configured.
+     */
+    public Map<String, Object> properties() {
+        return properties;
+    }
+
+    /**
+     * Sets a {@link Map} of properties to convey to the remote when a new {@link Connection}
+     * is created from these {@link ConnectionOptions}.
+     *
+     * @param properties the properties to set
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions properties(Map<String, Object> properties) {
+        this.properties = properties;
+        return this;
+    }
+
+    /**
+     * @return the virtual host value configured.
+     */
+    public String virtualHost() {
+        return virtualHost;
+    }
+
+    /**
+     * The virtual host value to provide to the remote when creating a new {@link Connection}.
+     *
+     * @param virtualHost
+     * 		the virtual host to set
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions virtualHost(String virtualHost) {
+        this.virtualHost = virtualHost;
+        return this;
+    }
+
+    /**
+     * @return the user name that is configured for new {@link Connection} instances.
+     */
+    public String user() {
+        return user;
+    }
+
+    /**
+     * Sets the user name used when performing connection authentication.
+     *
+     * @param user the user to set
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions user(String user) {
+        this.user = user;
+        return this;
+    }
+
+    /**
+     * @return the password that is configured for new {@link Connection} instances.
+     */
+    public String password() {
+        return password;
+    }
+
+    /**
+     * Sets the password used when performing connection authentication.
+     *
+     * @param password the password to set
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions password(String password) {
+        this.password = password;
+        return this;
+    }
+
+    /**
+     * @return the transport options that will be used for the {@link Connection}.
+     */
+    public TransportOptions transportOptions() {
+        return transport;
+    }
+
+    /**
+     * @return the SSL options that will be used for the {@link Connection}.
+     */
+    public SslOptions sslOptions() {
+        return ssl;
+    }
+
+    /**
+     * @return the SASL options that will be used for the {@link Connection}.
+     */
+    public SaslOptions saslOptions() {
+        return sasl;
+    }
+
+    /**
+     * @return true if reconnection support has been enabled for this connection.
+     */
+    public boolean reconnectEnabled() {
+        return reconnect.reconnectEnabled();
+    }
+
+    /**
+     * Controls if the connection will attempt to reconnect if unable to connect immediately
+     * or if an existing connection fails.
+     * <p>
+     * This option enables or disables reconnection to a remote remote peer after IO errors.
+     * To control specifics of the reconnection configuration for the {@link Connection} the
+     * values must be updated in the {@link ReconnectOptions} configuration prior to creating
+     * the connection.
+     *
+     * @param reconnectEnabled
+     *      Controls if reconnection is enabled or not for the associated {@link Connection}.
+     *
+     * @return this options instance.
+     */
+    public ConnectionOptions reconnectEnabled(boolean reconnectEnabled) {
+        reconnect.reconnectEnabled(reconnectEnabled);
+        return this;
+    }
+
+    /**
+     * @return the reconnection options that will be used for the {@link Connection}.
+     */
+    public ReconnectOptions reconnectOptions() {
+        return reconnect;
+    }
+
+    /**
+     * Configure if the newly created connection should enabled AMQP frame tracing to the
+     * system output.
+     *
+     * @param traceFrames
+     * 		true if frame tracing on this connection should be enabled.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions traceFrames(boolean traceFrames) {
+        this.traceFrames = traceFrames;
+        return this;
+    }
+
+    /**
+     * @return true if the connection is configured to perform frame tracing.
+     */
+    public boolean traceFrames() {
+        return this.traceFrames;
+    }
+
+    /**
+     * @return true if SSL support has been enabled for this connection.
+     */
+    public boolean sslEnabled() {
+        return ssl.sslEnabled();
+    }
+
+    /**
+     * Controls if the connection will attempt to connect using a secure IO layer or not.
+     * <p>
+     * This option enables or disables SSL encryption when connecting to a remote peer.  To
+     * control specifics of the SSL configuration for the {@link Connection} the values must
+     * be updated in the {@link SslOptions} configuration prior to creating the connection.
+     *
+     * @param sslEnabled
+     * 		Is SSL encryption enabled for the {@link Connection}.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     */
+    public ConnectionOptions sslEnabled(boolean sslEnabled) {
+        ssl.sslEnabled(sslEnabled);
+        return this;
+    }
+
+    /**
+     * @return the connection failed handler currently registered.
+     */
+    public BiConsumer<Connection, DisconnectionEvent> disconnectedHandler() {
+        return disconnectedHandler;
+    }
+
+    /**
+     * Configures a handler that will be notified when the connection has failed and cannot be recovered
+     * should reconnect be enabled.  Once notified of the failure the {@link Connection} is no longer
+     * operable and the {@link Connection} APIs will throw an exception to indicate that the connection
+     * has failed.  The client application should close a failed {@link Connection} once it becomes
+     * aware of the failure to ensure all connection resources are cleaned up properly.
+     *
+     * @param disconnectedHandler
+     *      the connection failed handler to notify when the connection fails for any reason.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     *
+     * @see #connectionInterrupted
+     * @see #connectionRestored
+     * @see #connectionFailed
+     */
+    public ConnectionOptions disconnectedHandler(BiConsumer<Connection, DisconnectionEvent> disconnectedHandler) {
+        this.disconnectedHandler = disconnectedHandler;
+        return this;
+    }
+
+    /**
+     * @return the connection established handler that is currently registered
+     */
+    public BiConsumer<Connection, ConnectionEvent> connectedHandler() {
+        return connectedhedHandler;
+    }
+
+    /**
+     * Configures a handler that will be notified when a {@link Connection} has established.
+     * This handler is called for each connection event when reconnection is enabled unless a
+     * {@link #reconnectedHandler} is configured in which case this handler is only notified
+     * on the first connection to a remote.
+     *
+     * @param connectedHandler
+     *      the connection established handler to assign to these {@link ConnectionOptions}.
+     *
+     * @return this {@link ConnectionOptions} instance.
+     *
+     * @see #disconnectedHandler()
+     * @see #interruptedHandler
+     * @see #reconnectedHandler
+     */
+    public ConnectionOptions connectedHandler(BiConsumer<Connection, ConnectionEvent> connectedHandler) {
+        this.connectedhedHandler = connectedHandler;
+        return this;
+    }
+
+    /**
+     * @return the connection interrupted handler that is currently registered
+     */
+    public BiConsumer<Connection, DisconnectionEvent> interruptedHandler() {
+        return interruptedHandler;
+    }
+
+    /**
+     * Configures a handler that will be notified when the current {@link Connection} experiences an
+     * interruption.  The {@link Connection} will only signal this handler when the reconnection feature
+     * is enabled and will follow this event either with a notification that the connection has been
+     * restored (if a handler is registered), or with a notification that the connection has failed
+     * if the reconnection configuration places limits on the the number of reconnection attempts.
+     *
+     * @param interruptedHandler
+     *      the connection interrupted handler to assign to these {@link ConnectionOptions}.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     *
+     * @see #connectedhedHandler
+     * @see #reconnectedHandler
+     * @see #failedHandler
+     */
+    public ConnectionOptions interruptedHandler(BiConsumer<Connection, DisconnectionEvent> interruptedHandler) {
+        this.interruptedHandler = interruptedHandler;
+        return this;
+    }
+
+    /**
+     * @return the connection restored handler that is currently registered
+     */
+    public BiConsumer<Connection, ConnectionEvent> reconnectedHandler() {
+        return reconnectedHandler;
+    }
+
+    /**
+     * Configures a handler that will be notified when a {@link Connection} that has previously
+     * experienced and interruption has been reconnected to a remote based on the reconnection
+     * configuration.
+     *
+     * @param reconnectedHandler
+     *      the connection restored handler to assign to these {@link ConnectionOptions}.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     *
+     * @see #connectedhedHandler
+     * @see #interruptedHandler
+     * @see #failedHandler
+     */
+    public ConnectionOptions reconnectedHandler(BiConsumer<Connection, ConnectionEvent> reconnectedHandler) {
+        this.reconnectedHandler = reconnectedHandler;
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Delivery.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Delivery.java
new file mode 100644
index 0000000..495b656
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Delivery.java
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+
+/**
+ * Incoming Delivery type that provides access to the message and the delivery
+ * data along with methods for settling the delivery when processing completes.
+ */
+public interface Delivery {
+
+    /**
+     * @return the {@link Receiver} that originated this {@link Delivery}.
+     */
+    Receiver receiver();
+
+    /**
+     * Decode the {@link Delivery} payload and return an {@link Message} object.
+     * <p>
+     * If the incoming message carried any delivery annotations they can be accessed via the
+     * {@link #annotations()} method.  Re-sending the returned message will not also
+     * send the incoming delivery annotations, the sender must include them in the
+     * {@link Sender#send(Message, Map)} call if they are to be forwarded onto the next recipient.
+     * <p>
+     * Calling this message claims the payload of the delivery for the returned {@link Message} and
+     * excludes use of the {@link #rawInputStream()} method of the {@link Delivery} object.  Calling
+     * the {@link #rawInputStream()} method after calling this method throws {@link ClientIllegalStateException}.
+     *
+     * @return a {@link Message} instance that wraps the decoded payload.
+     *
+     * @throws ClientException if an error occurs while decoding the payload.
+     *
+     * @param <E> The type of message body that should be contained in the returned {@link Message}.
+     */
+    <E> Message<E> message() throws ClientException;
+
+    /**
+     * Create and return an {@link InputStream} that reads the raw payload bytes of the given {@link Delivery}.
+     * <p>
+     * Calling this method claims the payload of the delivery for the returned {@link InputStream} and excludes
+     * use of the {@link #message()} and {@link #annotations()} methods of the {@link Delivery} object.  Closing
+     * the returned input stream discards any unread bytes from the delivery payload.  Calling the {@link #message()}
+     * or {@link #annotations()} methods after calling this method throws {@link ClientIllegalStateException}.
+     *
+     * @return an {@link InputStream} instance that can be used to read the raw delivery payload.
+     *
+     * @throws ClientException if an error occurs while decoding the payload.
+     */
+    InputStream rawInputStream() throws ClientException;
+
+    /**
+     * Decodes the {@link Delivery} payload and returns a {@link Map} containing a copy
+     * of any associated {@link DeliveryAnnotations} that were transmitted with the {@link Message}
+     * payload of this {@link Delivery}.
+     * <p>
+     * Calling this message claims the payload of the delivery for the returned {@link Map} and the decoded
+     * {@link Message} that can be accessed via the {@link #message()} method and  excludes use of the
+     * {@link #rawInputStream()} method of the {@link Delivery} object.  Calling the {@link #rawInputStream()}
+     * method after calling this method throws {@link ClientIllegalStateException}.
+     *
+     * @return copy of the delivery annotations that were transmitted with the {@link Message} payload.
+     *
+     * @throws ClientException if an error occurs while decoding the payload.
+     */
+    Map<String, Object> annotations() throws ClientException;
+
+    /**
+     * Accepts and settles the delivery.
+     *
+     * @return this {@link Delivery} instance.
+     *
+     * @throws ClientException if an error occurs while sending the disposition
+     */
+    Delivery accept() throws ClientException;
+
+    /**
+     * Releases and settles the delivery.
+     *
+     * @return this {@link Delivery} instance.
+     *
+     * @throws ClientException if an error occurs while sending the disposition
+     */
+    Delivery release() throws ClientException;
+
+    /**
+     * Rejects and settles the delivery, sending supplied error information along
+     * with the rejection.
+     *
+     * @param condition
+     *      The error condition value to supply with the rejection.
+     * @param description
+     *      The error description value to supply with the rejection.
+     *
+     * @return this {@link Delivery} instance.
+     *
+     * @throws ClientException if an error occurs while sending the disposition
+     */
+    Delivery reject(String condition, String description) throws ClientException;
+
+    /**
+     * Modifies and settles the delivery.
+     *
+     * @param deliveryFailed
+     *      Indicates if the modified delivery failed.
+     * @param undeliverableHere
+     *      Indicates if the modified delivery should not be returned here again.
+     *
+     * @return this {@link Delivery} instance.
+     *
+     * @throws ClientException if an error occurs while sending the disposition
+     */
+    Delivery modified(boolean deliveryFailed, boolean undeliverableHere) throws ClientException;
+
+    /**
+     * Updates the DeliveryState, and optionally settle the delivery as well.
+     *
+     * @param state
+     *            the delivery state to apply
+     * @param settle
+     *            whether to {@link #settle()} the delivery at the same time
+     *
+     * @return this {@link Delivery} instance.
+     *
+     * @throws ClientException if an error occurs while sending the disposition
+     */
+    Delivery disposition(DeliveryState state, boolean settle) throws ClientException;
+
+    /**
+     * Settles the delivery locally.
+     *
+     * @return the delivery
+     *
+     * @throws ClientException if an error occurs while sending the disposition
+     */
+    Delivery settle() throws ClientException;
+
+    /**
+     * @return true if the delivery has been locally settled.
+     *
+     * @throws ClientException if an error occurs while reading the settled state
+     */
+    boolean settled() throws ClientException;
+
+    /**
+     * Gets the current local state for the delivery.
+     *
+     * @return the delivery state
+     *
+     * @throws ClientException if an error occurs while reading the delivery state
+     */
+    DeliveryState state() throws ClientException;
+
+    /**
+     * Gets the current remote state for the delivery.
+     *
+     * @return the remote delivery state
+     *
+     * @throws ClientException if an error occurs while reading the remote delivery state
+     */
+    DeliveryState remoteState() throws ClientException;
+
+    /**
+     * Gets whether the delivery was settled by the remote peer yet.
+     *
+     * @return whether the delivery is remotely settled
+     *
+     * @throws ClientException if an error occurs while reading the remote settlement state
+     */
+    boolean remoteSettled() throws ClientException;
+
+    /**
+     * Gets the message format for the current delivery.
+     *
+     * @return the message format
+     *
+     * @throws ClientException if an error occurs while reading the delivery message format
+     */
+    int messageFormat() throws ClientException;
+
+}
\ No newline at end of file
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DeliveryMode.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DeliveryMode.java
new file mode 100644
index 0000000..4ef9b66
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DeliveryMode.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * Control the message delivery guarantee for senders and receivers
+ */
+public enum DeliveryMode {
+    AT_MOST_ONCE,
+    AT_LEAST_ONCE
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DeliveryState.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DeliveryState.java
new file mode 100644
index 0000000..d7e86b1
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DeliveryState.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.impl.ClientDeliveryState.ClientAccepted;
+import org.apache.qpid.protonj2.client.impl.ClientDeliveryState.ClientModified;
+import org.apache.qpid.protonj2.client.impl.ClientDeliveryState.ClientRejected;
+import org.apache.qpid.protonj2.client.impl.ClientDeliveryState.ClientReleased;
+
+/**
+ * Conveys the outcome of a Delivery either incoming or outgoing.
+ */
+public interface DeliveryState {
+
+    public enum Type {
+        ACCEPTED,
+        REJECTED,
+        MODIFIED,
+        RELEASED,
+        TRANSACTIONAL
+    }
+
+    Type getType();
+
+    /**
+     * @return true if the {@link DeliveryState} represents an Accepted outcome.
+     */
+    default boolean isAccepted() {
+        return getType() == Type.ACCEPTED;
+    }
+
+    //----- Factory methods for default DeliveryState types
+
+    static DeliveryState accepted() {
+        return ClientAccepted.getInstance();
+    }
+
+    static DeliveryState released() {
+        return ClientReleased.getInstance();
+    }
+
+    static DeliveryState rejected(String condition, String description) {
+        return new ClientRejected(condition, description);
+    }
+
+    static DeliveryState rejected(String condition, String description, Map<String, Object> info) {
+        return new ClientRejected(condition, description, info);
+    }
+
+    static DeliveryState modified(boolean failed, boolean undeliverable) {
+        return new ClientModified(failed, undeliverable);
+    }
+
+    static DeliveryState modified(boolean failed, boolean undeliverable, Map<String, Object> annotations) {
+        return new ClientModified(failed, undeliverable, annotations);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DisconnectionEvent.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DisconnectionEvent.java
new file mode 100644
index 0000000..73caf4b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DisconnectionEvent.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+
+/**
+ * An event object that accompanies events fired to handlers configured in the
+ * {@link ConnectionOptions} which are signaled during specific {@link Connection}
+ * life-cycle stages.
+ */
+public class DisconnectionEvent {
+
+    private final String host;
+    private final int port;
+    private final ClientIOException failureCause;
+
+    /**
+     * Creates the event object with all immutable data provided.
+     *
+     * @param host
+     *      the host that is associated with this {@link DisconnectionEvent}
+     * @param port
+     *      the port that is associated with this {@link DisconnectionEvent}
+     * @param failureCause
+     *      the failure cause that is associated with this {@link DisconnectionEvent}
+     */
+    public DisconnectionEvent(String host, int port, ClientIOException failureCause) {
+        this.host = host;
+        this.port = port;
+        this.failureCause = failureCause;
+    }
+
+    /**
+     * Gets the host that is associated with this event which for a successful
+     * {@link Connection} event would be the currently active host and for an
+     * interrupted or failed Connection this host would indicate the host where
+     * the {@link Connection} had previously been established.
+     *
+     * @return the host that is associated with this {@link DisconnectionEvent}
+     */
+    public String host() {
+        return host;
+    }
+
+    /**
+     * Gets the port that is associated with this event which for a successful
+     * {@link Connection} event would be the currently active port and for an
+     * interrupted or failed Connection this port would indicate the host where
+     * the {@link Connection} had previously been established.
+     *
+     * @return the port that is associated with this {@link DisconnectionEvent}
+     */
+    public int port() {
+        return port;
+    }
+
+    /**
+     * Gets the failure cause that is associated with this event if the event indicates
+     * an error stage in the {@link Connection} life-cycle such as a connection being
+     * interrupted which might later be recovered or a failure to establish or reconnect
+     * a previously established {@link Connection}.
+     *
+     * @return the failureCause that is associated with this {@link DisconnectionEvent}
+     */
+    public ClientIOException failureCause() {
+        return failureCause;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DistributionMode.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DistributionMode.java
new file mode 100644
index 0000000..0704333
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DistributionMode.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * Control whether messages are browsed or consumed.
+ */
+public enum DistributionMode {
+    COPY,
+    MOVE
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DurabilityMode.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DurabilityMode.java
new file mode 100644
index 0000000..3f6935c
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/DurabilityMode.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * Control the persistence of source or target state.
+ */
+public enum DurabilityMode {
+    NONE,
+    CONFIGURATION,
+    UNSETTLED_STATE
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ErrorCondition.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ErrorCondition.java
new file mode 100644
index 0000000..02e1f2b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ErrorCondition.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.impl.ClientErrorCondition;
+
+/**
+ * Conveys the error value used to inform the user of why an endpoint
+ * was closed or a delivery rejected.
+ */
+public interface ErrorCondition {
+
+    /**
+     * @return a value that indicates the type of error condition.
+     */
+    String condition();
+
+    /**
+     * Descriptive text that supplies any supplementary details not indicated by the condition value..
+     *
+     * @return supplementary details not indicated by the condition value..
+     */
+    String description();
+
+    /**
+     * @return a {@link Map} carrying information about the error condition.
+     */
+    Map<String, Object> info();
+
+    //----- Factory Methods for Default Error Condition types
+
+    /**
+     * Create an error condition object using the supplied values.  The condition string
+     * cannot be null however the other attribute can.
+     *
+     * @param condition
+     *      The value that defines the error condition.
+     * @param description
+     *      The supplementary description for the given error condition.
+     *
+     * @return a new read-only {@link ErrorCondition} object
+     */
+    static ErrorCondition create(String condition, String description) {
+        return new ClientErrorCondition(condition, description, null);
+    }
+
+    /**
+     * Create an error condition object using the supplied values.  The condition string
+     * cannot be null however the other attribute can.
+     *
+     * @param condition
+     * 		The value that defines the error condition.
+     * @param description
+     * 		The supplementary description for the given error condition.
+     * @param info
+     * 		A {@link Map} containing additional error information.
+     *
+     * @return a new read-only {@link ErrorCondition} object
+     */
+    static ErrorCondition create(String condition, String description, Map<String, Object> info) {
+        return new ClientErrorCondition(condition, description, info);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ExpiryPolicy.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ExpiryPolicy.java
new file mode 100644
index 0000000..cfcf174
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ExpiryPolicy.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * Control when the clock for expiration begins.
+ */
+public enum ExpiryPolicy {
+    LINK_CLOSE,
+    SESSION_CLOSE,
+    CONNECTION_CLOSE,
+    NEVER
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Message.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Message.java
new file mode 100644
index 0000000..caefec5
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Message.java
@@ -0,0 +1,809 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.impl.ClientMessage;
+import org.apache.qpid.protonj2.client.impl.ClientMessageSupport;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Section;
+
+/**
+ * Message object that provides a high level abstraction to raw AMQP types
+ *
+ * @param <E> The type of the message body that this message carries
+ */
+public interface Message<E> {
+
+    /**
+     * Create and return an {@link Message} that will carry no body {@link Section}
+     * unless one is assigned by the caller.
+     *
+     * @return a new {@link Message} instance with an empty body {@link Section}.
+     */
+    static <E> Message<E> create() {
+        return ClientMessage.create();
+    }
+
+    /**
+     * Create and return an {@link Message} that will wrap the given {@link Object} in
+     * an {@link AmqpValue} section.
+     *
+     * @param body
+     *      An object that will be wrapped in an {@link AmqpValue} body section.
+     *
+     * @return a new {@link Message} instance with a body containing the given value.
+     */
+    static <E> Message<E> create(E body) {
+        return ClientMessage.create(new AmqpValue<>(body));
+    }
+
+    /**
+     * Create and return an {@link Message} that will wrap the given byte array in
+     * an {@link Data} section.
+     *
+     * @param body
+     *      An byte array that will be wrapped in an {@link Data} body section.
+     *
+     * @return a new {@link Message} instance with a body containing the given byte array.
+     */
+    static Message<byte[]> create(byte[] body) {
+        return ClientMessage.create(new Data(body));
+    }
+
+    /**
+     * Create and return an {@link Message} that will wrap the given {@link List} in
+     * an {@link AmqpSequence} section.
+     *
+     * @param body
+     *      An List that will be wrapped in an {@link AmqpSequence} body section.
+     *
+     * @return a new {@link Message} instance with a body containing the given List.
+     */
+    static <E> Message<List<E>> create(List<E> body) {
+        return ClientMessage.create(new AmqpSequence<>(body));
+    }
+
+    /**
+     * Create and return an {@link Message} that will wrap the given {@link Map} in
+     * an {@link AmqpValue} section.
+     *
+     * @param body
+     *      An Map that will be wrapped in an {@link AmqpValue} body section.
+     *
+     * @return a new {@link Message} instance with a body containing the given Map.
+     */
+    static <K, V> Message<Map<K, V>> create(Map<K, V> body) {
+        return ClientMessage.create(new AmqpValue<>(body));
+    }
+
+    //----- Message specific APIs
+
+    /**
+     * Safely convert this {@link Message} instance into an {@link AdvancedMessage} reference
+     * which can offer more low level APIs to an experienced client user.
+     * <p>
+     * The default implementation first checks if the current instance is already of the correct
+     * type before performing a brute force conversion of the current message to the client's
+     * own internal {@link AdvancedMessage} implementation.  Users should override this method
+     * if the internal conversion implementation is insufficient to obtain the proper message
+     * structure to encode a meaningful 'on the wire' encoding of their custom implementation.
+     *
+     * @return a {@link AdvancedMessage} that contains this message's current state.
+     *
+     * @throws UnsupportedOperationException if the {@link Message} implementation cannot be converted
+     * @throws ClientException if an error occurs while converting the message to an {@link AdvancedMessage}/
+     */
+    default AdvancedMessage<E> toAdvancedMessage() throws ClientException {
+        if (this instanceof AdvancedMessage) {
+            return (AdvancedMessage<E>) this;
+        } else {
+            return ClientMessageSupport.convertMessage(this);
+        }
+    }
+
+    //----- AMQP Header Section
+
+    /**
+     * For an message being sent this method returns the current state of the
+     * durable flag on the message.  For a received message this method returns
+     * the durable flag value at the time of sending (or false if not set) unless
+     * the value is updated after being received by the receiver.
+     *
+     * @return true if the Message is marked as being durable
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    boolean durable() throws ClientException;
+
+    /**
+     * Controls if the message is marked as durable when sent.
+     *
+     * @param durable
+     *      value assigned to the durable flag for this message.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> durable(boolean durable) throws ClientException;
+
+    /**
+     * @return the currently configured priority or the default if none set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    byte priority() throws ClientException;
+
+    /**
+     * Sets the relative message priority.  Higher numbers indicate higher priority messages.
+     * Messages with higher priorities MAY be delivered before those with lower priorities.
+     *
+     * @param priority
+     * 		The priority value to assign this message.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> priority(byte priority) throws ClientException;
+
+    /**
+     * @return the currently set Time To Live duration (milliseconds).
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    long timeToLive() throws ClientException;
+
+    /**
+     * Sets the message time to live value.
+     * <p>
+     * The time to live duration in milliseconds for which the message is to be considered "live".
+     * If this is set then a message expiration time will be computed based on the time of arrival
+     * at an intermediary. Messages that live longer than their expiration time will be discarded
+     * (or dead lettered). When a message is transmitted by an intermediary that was received with a
+     * time to live, the transmitted message's header SHOULD contain a time to live that is computed
+     * as the difference between the current time and the formerly computed message expiration time,
+     * i.e., the reduced time to live, so that messages will eventually die if they end up in a
+     * delivery loop.
+     *
+     * @param timeToLive
+     *      The time span in milliseconds that this message should remain live before being discarded.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> timeToLive(long timeToLive) throws ClientException;
+
+    /**
+     * @return if this message has been acquired by another link previously
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    boolean firstAcquirer() throws ClientException;
+
+    /**
+     * Sets the value to assign to the first acquirer field of this {@link Message}.
+     * <p>
+     * If this value is true, then this message has not been acquired by any other link.  If this
+     * value is false, then this message MAY have previously been acquired by another link or links.
+     *
+     * @param firstAcquirer
+     *      The boolean value to assign to the first acquirer field of the message.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> firstAcquirer(boolean firstAcquirer) throws ClientException;
+
+    /**
+     * @return the number of failed delivery attempts that this message has been part of.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    long deliveryCount() throws ClientException;
+
+    /**
+     * Sets the value to assign to the delivery count field of this {@link Message}.
+     * <p>
+     * Delivery count is the number of unsuccessful previous attempts to deliver this message.
+     * If this value is non-zero it can be taken as an indication that the delivery might be a
+     * duplicate. On first delivery, the value is zero. It is incremented upon an outcome being
+     * settled at the sender, according to rules defined for each outcome.
+     *
+     * @param deliveryCount
+     *      The new delivery count value to assign to this message.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> deliveryCount(long deliveryCount) throws ClientException;
+
+    //----- AMQP Properties Section
+
+    /**
+     * @return the currently set Message ID or null if none set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    Object messageId() throws ClientException;
+
+    /**
+     * Sets the message Id value to assign to this {@link Message}.
+     * <p>
+     * The message Id, if set, uniquely identifies a message within the message system. The message
+     * producer is usually responsible for setting the message-id in such a way that it is assured to
+     * be globally unique. A remote peer MAY discard a message as a duplicate if the value of the
+     * message-id matches that of a previously received message sent to the same node.
+     *
+     * @param messageId
+     *      The message Id value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> messageId(Object messageId) throws ClientException;
+
+    /**
+     * @return the currently set User ID or null if none set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    byte[] userId() throws ClientException;
+
+    /**
+     * Sets the user Id value to assign to this {@link Message}.
+     * <p>
+     * The identity of the user responsible for producing the message. The client sets this value,
+     * and it MAY be authenticated by intermediaries.
+     *
+     * @param userId
+     *      The user Id value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> userId(byte[] userId) throws ClientException;
+
+    /**
+     * @return the currently set 'To' address which indicates the intended destination of the message.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String to() throws ClientException;
+
+    /**
+     * Sets the 'to' value to assign to this {@link Message}.
+     * <p>
+     * The to field identifies the node that is the intended destination of the message. On any given
+     * transfer this might not be the node at the receiving end of the link.
+     *
+     * @param to
+     *      The 'to' node value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> to(String to) throws ClientException;
+
+    /**
+     * @return the currently set subject metadata for this message or null if none set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String subject() throws ClientException;
+
+    /**
+     * Sets the subject value to assign to this {@link Message}.
+     * <p>
+     * A common field for summary information about the message content and purpose.
+     *
+     * @param subject
+     *      The subject node value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> subject(String subject) throws ClientException;
+
+    /**
+     * @return the configured address of the node where replies to this message should be sent, or null if not set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String replyTo() throws ClientException;
+
+    /**
+     * Sets the replyTo value to assign to this {@link Message}.
+     * <p>
+     * The address of the node to send replies to.
+     *
+     * @param replyTo
+     *      The replyTo node value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> replyTo(String replyTo) throws ClientException;
+
+    /**
+     * @return the currently assigned correlation ID or null if none set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    Object correlationId() throws ClientException;
+
+    /**
+     * Sets the correlationId value to assign to this {@link Message}.
+     * <p>
+     * This is a client-specific id that can be used to mark or identify messages between clients.
+     *
+     * @param correlationId
+     *      The correlationId value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> correlationId(Object correlationId) throws ClientException;
+
+    /**
+     * @return the assigned content type value for the message body section or null if not set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String contentType() throws ClientException;
+
+    /**
+     * Sets the contentType value to assign to this {@link Message}.
+     * <p>
+     * The RFC-2046 MIME type for the message's application-data section (body). As per RFC-2046 this can
+     * contain a charset parameter defining the character encoding used: e.g., 'text/plain; charset="utf-8"'.
+     * <p>
+     * For clarity, as per section 7.2.1 of RFC-2616, where the content type is unknown the content-type
+     * SHOULD NOT be set. This allows the recipient the opportunity to determine the actual type. Where the
+     * section is known to be truly opaque binary data, the content-type SHOULD be set to application/octet-stream.
+     * <p>
+     * When using an application-data section with a section code other than data, content-type SHOULD NOT be set.
+     *
+     * @param contentType
+     *      The contentType value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> contentType(String contentType) throws ClientException;
+
+    /**
+     * @return the assigned content encoding value for the message body section or null if not set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String contentEncoding() throws ClientException;
+
+    /**
+     * Sets the contentEncoding value to assign to this {@link Message}.
+     * <p>
+     * The content-encoding property is used as a modifier to the content-type. When present, its value
+     * indicates what additional content encodings have been applied to the application-data, and thus what
+     * decoding mechanisms need to be applied in order to obtain the media-type referenced by the content-type
+     * header field.
+     * <p>
+     * Content-encoding is primarily used to allow a document to be compressed without losing the identity of
+     * its underlying content type.
+     * <p>
+     * Content-encodings are to be interpreted as per section 3.5 of RFC 2616 [RFC2616]. Valid content-encodings
+     * are registered at IANA [IANAHTTPPARAMS].
+     * <p>
+     * The content-encoding MUST NOT be set when the application-data section is other than data. The binary
+     * representation of all other application-data section types is defined completely in terms of the AMQP
+     * type system.
+     * <p>
+     * Implementations MUST NOT use the identity encoding. Instead, implementations SHOULD NOT set this property.
+     * Implementations SHOULD NOT use the compress encoding, except as to remain compatible with messages originally
+     * sent with other protocols, e.g. HTTP or SMTP.
+     * <p>
+     * Implementations SHOULD NOT specify multiple content-encoding values except as to be compatible with messages
+     * originally sent with other protocols, e.g. HTTP or SMTP.
+     * <p>
+     *
+     * @param contentEncoding
+     *      The contentEncoding value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<?> contentEncoding(String contentEncoding) throws ClientException;
+
+    /**
+     * @return the configured absolute time of expiration for this message.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    long absoluteExpiryTime() throws ClientException;
+
+    /**
+     * Sets the absolute expiration time value to assign to this {@link Message}.
+     * <p>
+     * An absolute time when this message is considered to be expired.
+     *
+     * @param expiryTime
+     *      The absolute expiration time value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> absoluteExpiryTime(long expiryTime) throws ClientException;
+
+    /**
+     * @return the absolute time of creation for this message.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    long creationTime() throws ClientException;
+
+    /**
+     * Sets the creation time value to assign to this {@link Message}.
+     * <p>
+     * An absolute time when this message was created.
+     *
+     * @param createTime
+     *      The creation time value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> creationTime(long createTime) throws ClientException;
+
+    /**
+     * @return the assigned group ID for this message or null if not set.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String groupId() throws ClientException;
+
+    /**
+     * Sets the groupId value to assign to this {@link Message}.
+     * <p>
+     * Identifies the group the message belongs to.
+     *
+     * @param groupId
+     *      The groupId value to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> groupId(String groupId) throws ClientException;
+
+    /**
+     * @return the assigned group sequence for this message.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    int groupSequence() throws ClientException;
+
+    /**
+     * Sets the group sequence value to assign to this {@link Message}.
+     * <p>
+     * The relative position of this message within its group.
+     *
+     * @param groupSequence
+     *      The group sequence to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> groupSequence(int groupSequence) throws ClientException;
+
+    /**
+     * @return the client-specific id used so that client can send replies to this message to a specific group.
+     *
+     * @throws ClientException if an error occurs while reading the given value.
+     */
+    String replyToGroupId() throws ClientException;
+
+    /**
+     * Sets the replyTo group Id value to assign to this {@link Message}.
+     * <p>
+     * This is a client-specific id that is used so that client can send replies to this message
+     * to a specific group.
+     *
+     * @param replyToGroupId
+     *      The replyTo group Id to assign to this {@link Message} instance.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs while writing the given value.
+     */
+    Message<E> replyToGroupId(String replyToGroupId) throws ClientException;
+
+    //----- Message Annotations
+
+    /**
+     * Returns the requested message annotation value from this {@link Message} if it exists
+     * or returns null otherwise.
+     *
+     * @param key
+     *      The key of the message annotation to query for.
+     *
+     * @return the corresponding message annotation value of null if none was carried in this {@link Message}.
+     *
+     * @throws ClientException if an error occurs accessing the message annotations in this {@link Message}.
+     */
+    Object annotation(String key) throws ClientException;
+
+    /**
+     * Query the {@link Message} to determine if it carries the given message annotation key.
+     *
+     * @param key
+     *      The key of the message annotation to query for.
+     *
+     * @return <code>true</code> if the Message carries the given message annotation.
+     *
+     * @throws ClientException if an error occurs accessing the message annotations in this {@link Message}.
+     */
+    boolean hasAnnotation(String key) throws ClientException;
+
+    /**
+     * Query the {@link Message} to determine if it carries any message annotations.
+     *
+     * @return <code>true</code> if the Message carries any message annotations.
+     *
+     * @throws ClientException if an error occurs accessing the message annotations in this {@link Message}.
+     */
+    boolean hasAnnotations() throws ClientException;
+
+    /**
+     * Removes the given message annotation from the values carried in the message currently, if none
+     * was present than this method returns <code>null</code>.
+     *
+     * @param key
+     *      The key of the message annotation to query for removal.
+     *
+     * @return the message annotation value that was previously assigned to that key.
+     *
+     * @throws ClientException if an error occurs accessing the message annotations in this {@link Message}.
+     */
+    Object removeAnnotation(String key) throws ClientException;
+
+    /**
+     * Invokes the given {@link BiConsumer} on each message annotation entry carried in this {@link Message}.
+     *
+     * @param action
+     *      The action that will be invoked on each message annotation entry.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs accessing the message annotations in this {@link Message}.
+     */
+    Message<E> forEachAnnotation(BiConsumer<String, Object> action) throws ClientException;
+
+    /**
+     * Sets the given message annotation value at the given key, replacing any previous value
+     * that was assigned to this {@link Message}.
+     *
+     * @param key
+     *      The message annotation key where the value is to be assigned.
+     * @param value
+     *      The value to assign to the given message annotation key.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs accessing the message annotations in this {@link Message}.
+     */
+    Message<E> annotation(String key, Object value) throws ClientException;
+
+    //----- Application Properties
+
+    /**
+     * Returns the requested application property value from this {@link Message} if it exists
+     * or returns null otherwise.
+     *
+     * @param key
+     *      The key of the application property to query for.
+     *
+     * @return the corresponding application property value of null if none was carried in this {@link Message}.
+     *
+     * @throws ClientException if an error occurs accessing the application properties in this {@link Message}.
+     */
+    Object property(String key) throws ClientException;
+
+    /**
+     * Sets the given application property value at the given key, replacing any previous value
+     * that was assigned to this {@link Message}.
+     *
+     * @param key
+     *      The application property key where the value is to be assigned.
+     * @param value
+     *      The value to assign to the given application property key.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs accessing the application properties in this {@link Message}.
+     */
+    Message<E> property(String key, Object value) throws ClientException;
+
+    /**
+     * Query the {@link Message} to determine if it carries the given application property key.
+     *
+     * @param key
+     *      The key of the application property to query for.
+     *
+     * @return <code>true</code> if the Message carries the given application property.
+     *
+     * @throws ClientException if an error occurs accessing the application properties in this {@link Message}.
+     */
+    boolean hasProperty(String key) throws ClientException;
+
+    /**
+     * Query the {@link Message} to determine if it carries any application properties.
+     *
+     * @return <code>true</code> if the Message carries any application properties.
+     *
+     * @throws ClientException if an error occurs accessing the application properties in this {@link Message}.
+     */
+    boolean hasProperties() throws ClientException;
+
+    /**
+     * Removes the given application property from the values carried in the message currently, if none
+     * was present than this method returns <code>null</code>.
+     *
+     * @param key
+     *      The key of the application property to query for removal.
+     *
+     * @return the application property value that was previously assigned to that key.
+     *
+     * @throws ClientException if an error occurs accessing the application properties in this {@link Message}.
+     */
+    Object removeProperty(String key) throws ClientException;
+
+    /**
+     * Invokes the given {@link BiConsumer} on each application property entry carried in this {@link Message}.
+     *
+     * @param action
+     *      The action that will be invoked on each application property entry.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs accessing the application properties in this {@link Message}.
+     */
+    Message<E> forEachProperty(BiConsumer<String, Object> action) throws ClientException;
+
+    //----- Footer
+
+    /**
+     * Returns the requested footer value from this {@link Message} if it exists or returns
+     * <code>null</code> otherwise.
+     *
+     * @param key
+     *      The key of the footer to query for.
+     *
+     * @return the corresponding footer value of null if none was carried in this {@link Message}.
+     *
+     * @throws ClientException if an error occurs accessing the footers in this {@link Message}.
+     */
+    Object footer(String key) throws ClientException;
+
+    /**
+     * Query the {@link Message} to determine if it carries the given footer key.
+     *
+     * @param key
+     *      The key of the footer to query for.
+     *
+     * @return <code>true</code> if the Message carries the given footer.
+     *
+     * @throws ClientException if an error occurs accessing the footers in this {@link Message}.
+     */
+    boolean hasFooter(String key) throws ClientException;
+
+    /**
+     * Query the {@link Message} to determine if it carries any footers.
+     *
+     * @return <code>true</code> if the Message carries any footers.
+     *
+     * @throws ClientException if an error occurs accessing the footers in this {@link Message}.
+     */
+    boolean hasFooters() throws ClientException;
+
+    /**
+     * Removes the given footer from the values carried in the message currently, if none
+     * was present than this method returns <code>null</code>.
+     *
+     * @param key
+     *      The key of the footer to query for removal.
+     *
+     * @return the footer value that was previously assigned to that key.
+     *
+     * @throws ClientException if an error occurs accessing the footers in this {@link Message}.
+     */
+    Object removeFooter(String key) throws ClientException;
+
+    /**
+     * Invokes the given {@link BiConsumer} on each footer entry carried in this {@link Message}.
+     *
+     * @param action
+     *      The action that will be invoked on each footer entry.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs accessing the footers in this {@link Message}.
+     */
+    Message<E> forEachFooter(BiConsumer<String, Object> action) throws ClientException;
+
+    /**
+     * Sets the given footer value at the given key, replacing any previous value
+     * that was assigned to this {@link Message}.
+     *
+     * @param key
+     *      The footer key where the value is to be assigned.
+     * @param value
+     *      The value to assign to the given footer key.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if an error occurs accessing the footers in this {@link Message}.
+     */
+    Message<E> footer(String key, Object value) throws ClientException;
+
+    //----- AMQP Body Section
+
+    /**
+     * Returns the body value that is conveyed in this message or null if no body was set locally
+     * or sent from the remote if this is an incoming message.
+     *
+     * @return the message body value or null if none present.
+     *
+     * @throws ClientException if the implementation can't provide a body for some reason.
+     */
+    E body() throws ClientException;
+
+    /**
+     * Sets the body value that is to be conveyed to the remote when this message is sent.
+     * <p>
+     * The {@link Message} implementation will choose the AMQP {@link Section} to use to wrap
+     * the given value.
+     *
+     * @param value
+     *      The value to assign to the given message body {@link Section}.
+     *
+     * @return this {@link Message} instance.
+     *
+     * @throws ClientException if the implementation cannot write to the body section for some reason..
+     */
+    Message<E> body(E value) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/OutputStreamOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/OutputStreamOptions.java
new file mode 100644
index 0000000..6cf756e
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/OutputStreamOptions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.io.OutputStream;
+
+import org.apache.qpid.protonj2.types.messaging.Footer;
+
+/**
+ * Options class that controls various aspects of a {@link OutputStream} instance created to write
+ * the contents of a section of a {@link StreamSenderMessage}.
+ */
+public class OutputStreamOptions {
+
+    /**
+     * Defines the default value for the complete parent {@link StreamSenderMessage} on close option
+     */
+    public static final boolean DEFAULT_COMPLETE_SEND_ON_CLOSE = true;
+
+    private int streamSize;
+    private boolean completeSendOnClose = DEFAULT_COMPLETE_SEND_ON_CLOSE;
+
+    /**
+     * Creates a {@link OutputStreamOptions} instance with default values for all options
+     */
+    public OutputStreamOptions() {
+    }
+
+    /**
+     * Create a {@link OutputStreamOptions} instance that copies all configuration from the given
+     * {@link OutputStreamOptions} instance.
+     *
+     * @param options
+     *      The options instance to copy all configuration values from.
+     */
+    public OutputStreamOptions(OutputStreamOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    @Override
+    public OutputStreamOptions clone() {
+        return copyInto(new OutputStreamOptions());
+    }
+
+    /**
+     * Copy all options from this {@link OutputStreamOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this {@link OutputStreamOptions} class for chaining.
+     */
+    protected OutputStreamOptions copyInto(OutputStreamOptions other) {
+        other.bodyLength(streamSize);
+        other.completeSendOnClose(completeSendOnClose);
+
+        return this;
+    }
+
+    /**
+     * @return the configured stream size limit for associated {@link OutputStream}
+     */
+    public int bodyLength() {
+        return streamSize;
+    }
+
+    /**
+     * Sets the overall stream size for this associated {@link OutputStream} that the
+     * options are applied to.
+     * <p>
+     * When set this option indicates the number of bytes that can be written to the stream before an error
+     * would be thrown indicating that this value was exceeded.  Conversely if the stream is closed before
+     * the number of bytes indicated is written the send will be aborted and an error will be thrown to the
+     * caller.
+     *
+     * @param streamSize
+     *
+     * @return this {@link OutputStreamOptions} instance.
+     */
+    public OutputStreamOptions bodyLength(int streamSize) {
+        if (streamSize < 0) {
+            throw new IllegalArgumentException("Cannot set a stream body size that is negative");
+        }
+
+        this.streamSize = streamSize;
+        return this;
+    }
+
+    /**
+     * @return the whether the close of the {@link OutputStream} should complete the parent {@link StreamSenderMessage}
+     */
+    public boolean completeSendOnClose() {
+        return completeSendOnClose;
+    }
+
+    /**
+     * Configures if the close of the {@link OutputStream} should result in a completion of the parent
+     * {@link StreamSenderMessage} (default is true).  If there is a configured stream size and the {@link OutputStream}
+     * is closed the parent {@link StreamSenderMessage} will always be aborted as the send would be incomplete, but the
+     * close of an {@link OutputStream} may not always be the desired outcome.  In the case the user wishes to
+     * add a {@link Footer} to the message transmitted by the {@link StreamSenderMessage} this option should be set to
+     * false and the user should complete the stream manually.
+     *
+     * @param completeContextOnClose
+     *      Should the {@link OutputStream#close()} method complete the parent {@link StreamSenderMessage}
+     *
+     * @return this {@link OutputStreamOptions} instance.
+     */
+    public OutputStreamOptions completeSendOnClose(boolean completeContextOnClose) {
+        this.completeSendOnClose = completeContextOnClose;
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Receiver.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Receiver.java
new file mode 100644
index 0000000..8644cf2
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Receiver.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * AMQP Receiver that provides an interface to receive complete Deliveries from a remote
+ * peer.  Deliveries that are returned from the {@link #receive()} methods will be complete
+ * and can be read immediately without blocking waiting for additional delivery data to arrive.
+ *
+ * @see StreamReceiver
+ */
+public interface Receiver extends AutoCloseable {
+
+    /**
+     * @return a {@link Future} that will be completed when the remote opens this {@link Receiver}.
+     */
+    Future<Receiver> openFuture();
+
+    /**
+     * Requests a close of the {@link Receiver} at the remote and waits until the Receiver has been
+     * fully closed or until the configured {@link ReceiverOptions#closeTimeout()} is exceeded.
+     */
+    @Override
+    void close();
+
+    /**
+     * Requests a close of the {@link Receiver} at the remote and waits until the Receiver has been
+     * fully closed or until the configured {@link ReceiverOptions#closeTimeout()} is exceeded.
+     *
+     * @param error
+     *      The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     */
+    void close(ErrorCondition error);
+
+    /**
+     * Requests a detach of the {@link Receiver} at the remote and waits until the Receiver has been
+     * fully detached or until the configured {@link SenderOptions#closeTimeout()} is exceeded.
+     */
+    void detach();
+
+    /**
+     * Requests a detach of the {@link Receiver} at the remote and waits until the Receiver has been
+     * fully detached or until the configured {@link SenderOptions#closeTimeout()} is exceeded.
+     *
+     * @param error
+     *      The {@link ErrorCondition} to transmit to the remote along with the detach operation.
+     */
+    void detach(ErrorCondition error);
+
+    /**
+     * Requests a close of the {@link Receiver} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been closed.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Receiver} link.
+     */
+    Future<Receiver> closeAsync();
+
+    /**
+     * Requests a close of the {@link Receiver} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been closed.
+     *
+     * @param error
+     * 		The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Receiver} link.
+     */
+    Future<Receiver> closeAsync(ErrorCondition error);
+
+    /**
+     * Requests a detach of the {@link Receiver} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been detached.
+     *
+     * @return a {@link Future} that will be completed when the remote detaches this {@link Receiver} link.
+     */
+    Future<Receiver> detachAsync();
+
+    /**
+     * Requests a detach of the {@link Receiver} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been detached.
+     *
+     * @param error
+     * 		The {@link ErrorCondition} to transmit to the remote along with the detach operation.
+     *
+     * @return a {@link Future} that will be completed when the remote detaches this {@link Receiver} link.
+     */
+    Future<Receiver> detachAsync(ErrorCondition error);
+
+    /**
+     * Returns the address that the {@link Receiver} instance will be subscribed to.
+     *
+     * <ul>
+     *  <li>
+     *   If the Receiver was created with the dynamic receiver methods then the method will return
+     *   the dynamically created address once the remote has attached its end of the receiver link.
+     *   Due to the need to await the remote peer to populate the dynamic address this method will
+     *   block until the open of the receiver link has completed.
+     *  </li>
+     *  <li>
+     *   If not a dynamic receiver then the address returned is the address passed to the original
+     *   {@link Session#openReceiver(String)} or {@link Session#openReceiver(String, ReceiverOptions)} methods.
+     *  </li>
+     * </ul>
+     *
+     * @return the address that this {@link Receiver} is sending to.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Receiver} address.
+     */
+    String address() throws ClientException;
+
+    /**
+     * Returns an immutable view of the remote {@link Source} object assigned to this receiver link.  If the
+     * attach has not completed yet this method will block to await the attach response which carries the remote
+     * {@link Source}.
+     *
+     * @return the remote {@link Source} node configuration.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Receiver} remote {@link Source}.
+     */
+    Source source() throws ClientException;
+
+    /**
+     * Returns an immutable view of the remote {@link Target} object assigned to this receiver link.  If the
+     * attach has not completed yet this method will block to await the attach response which carries the remote
+     * {@link Source}.
+     *
+     * @return the remote {@link Target} node configuration.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Receiver} remote {@link Target}.
+     */
+    Target target() throws ClientException;
+
+    /**
+     * Returns the properties that the remote provided upon successfully opening the {@link Receiver}.  If the
+     * attach has not completed yet this method will block to await the attach response which carries the remote
+     * properties.  If the remote provides no properties this method will return null.
+     *
+     * @return any properties provided from the remote once the receiver has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Receiver} remote properties.
+     */
+    Map<String, Object> properties() throws ClientException;
+
+    /**
+     * Returns the offered capabilities that the remote provided upon successfully opening the {@link Receiver}.
+     * If the attach has not completed yet this method will block to await the attach response which carries the
+     * remote offered capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any capabilities provided from the remote once the receiver has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Receiver} remote offered capabilities.
+     */
+    String[] offeredCapabilities() throws ClientException;
+
+    /**
+     * Returns the desired capabilities that the remote provided upon successfully opening the {@link Receiver}.
+     * If the attach has not completed yet this method will block to await the attach response which carries the
+     * remote desired capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any desired capabilities provided from the remote once the receiver has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Receiver} remote desired capabilities.
+     */
+    String[] desiredCapabilities() throws ClientException;
+
+    /**
+     * @return the {@link Client} instance that holds this session's {@link Receiver}
+     */
+    Client client();
+
+    /**
+     * @return the {@link Connection} instance that holds this session's {@link Receiver}
+     */
+    Connection connection();
+
+    /**
+     * @return the {@link Session} that created and holds this {@link Receiver}.
+     */
+    Session session();
+
+    /**
+     * Adds credit to the {@link Receiver} link for use when there receiver has not been configured
+     * with a credit window.  When credit window is configured credit replenishment is automatic and
+     * calling this method will result in an exception indicating that the operation is invalid.
+     * <p>
+     * If the {@link Receiver} is draining and this method is called an exception will be thrown
+     * to indicate that credit cannot be replenished until the remote has drained the existing link
+     * credit.
+     *
+     * @param credits
+     *      The number of credits to add to the {@link Receiver} link.
+     *
+     * @return this {@link Receiver} instance.
+     *
+     * @throws ClientException if an error occurs while attempting to add new {@link Receiver} link credit.
+     */
+    Receiver addCredit(int credits) throws ClientException;
+
+    /**
+     * Blocking receive method that waits forever for the remote to provide a {@link Delivery} for consumption.
+     * <p>
+     * Receive calls will only grant credit on their own if a credit window is configured in the
+     * {@link ReceiverOptions} which is done by default.  If the client application has configured
+     * no credit window than this method will not grant any credit when it enters the wait for new
+     * incoming messages.
+     *
+     * @return a new {@link Delivery} received from the remote.
+     *
+     * @throws ClientException if the {@link Receiver} or its parent is closed when the call to receive is made.
+     */
+    Delivery receive() throws ClientException;
+
+    /**
+     * Blocking receive method that waits the given time interval for the remote to provide a
+     * {@link Delivery} for consumption.  The amount of time this method blocks is based on the
+     * timeout value. If timeout is equal to <code>-1</code> then it blocks until a Delivery is
+     * received. If timeout is equal to zero then it will not block and simply return a
+     * {@link Delivery} if one is available locally.  If timeout value is greater than zero then it
+     * blocks up to timeout amount of time.
+     * <p>
+     * Receive calls will only grant credit on their own if a credit window is configured in the
+     * {@link ReceiverOptions} which is done by default.  If the client application has not configured
+     * a credit window or granted credit manually this method will not automatically grant any credit
+     * when it enters the wait for a new incoming {@link Delivery}.
+     *
+     * @param timeout
+     *      The timeout value used to control how long the receive method waits for a new {@link Delivery}.
+     * @param unit
+     *      The unit of time that the given timeout represents.
+     *
+     * @return a new {@link Delivery} received from the remote.
+     *
+     * @throws ClientException if the {@link Receiver} or its parent is closed when the call to receive is made.
+     */
+    Delivery receive(long timeout, TimeUnit unit) throws ClientException;
+
+    /**
+     * Non-blocking receive method that either returns a message is one is immediately available or
+     * returns null if none is currently at hand.
+     *
+     * @return a new {@link Delivery} received from the remote or null if no pending deliveries are available.
+     *
+     * @throws ClientException if the {@link Receiver} or its parent is closed when the call to try to receive is made.
+     */
+    Delivery tryReceive() throws ClientException;
+
+    /**
+     * Requests the remote to drain previously granted credit for this {@link Receiver} link.
+     *
+     * @return a {@link Future} that will be completed when the remote drains this {@link Receiver} link.
+     *
+     * @throws ClientException if an error occurs while attempting to drain the link credit.
+     */
+    Future<Receiver> drain() throws ClientException;
+
+    /**
+     * Returns the number of Deliveries that are currently held in the {@link Receiver} delivery
+     * queue.  This number is likely to change immediately following the call as more deliveries
+     * arrive but can be used to determine if any pending {@link Delivery} work is ready.
+     *
+     * @return the number of deliveries that are currently buffered locally.
+     *
+     * @throws ClientException if an error occurs while attempting to fetch the queue count.
+     */
+    long queuedDeliveries() throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ReceiverOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ReceiverOptions.java
new file mode 100644
index 0000000..71cfdcd
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ReceiverOptions.java
@@ -0,0 +1,354 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+
+/**
+ * Options that control the behavior of the {@link Receiver} created from them.
+ */
+public class ReceiverOptions {
+
+    private long drainTimeout = ConnectionOptions.DEFAULT_DRAIN_TIMEOUT;
+    private long requestTimeout = ConnectionOptions.DEFAULT_REQUEST_TIMEOUT;
+    private long openTimeout = ConnectionOptions.DEFAULT_OPEN_TIMEOUT;
+    private long closeTimeout = ConnectionOptions.DEFAULT_CLOSE_TIMEOUT;
+
+    private boolean autoAccept = true;
+    private boolean autoSettle = true;
+    private DeliveryMode deliveryMode = DeliveryMode.AT_LEAST_ONCE;
+    private int creditWindow = 10;
+    private String linkName;
+
+    private final SourceOptions source = new SourceOptions();
+    private final TargetOptions target = new TargetOptions();
+
+    private String[] offeredCapabilities;
+    private String[] desiredCapabilities;
+    private Map<String, Object> properties;
+
+    public ReceiverOptions() {
+    }
+
+    public ReceiverOptions(ReceiverOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    /**
+     * Controls if the created Receiver will automatically accept the deliveries that have
+     * been received by the application (default is <code>true</code>).
+     *
+     * @param autoAccept
+     *      The value to assign for auto delivery acceptance.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions autoAccept(boolean autoAccept) {
+        this.autoAccept = autoAccept;
+        return this;
+    }
+
+    /**
+     * @return the current value of the {@link Receiver} auto accept setting.
+     */
+    public boolean autoAccept() {
+        return autoAccept;
+    }
+
+    /**
+     * Controls if the created Receiver will automatically settle the deliveries that have
+     * been received by the application (default is <code>true</code>).
+     *
+     * @param autoSettle
+     *      The value to assign for auto delivery settlement.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions autoSettle(boolean autoSettle) {
+        this.autoSettle = autoSettle;
+        return this;
+    }
+
+    /**
+     * @return the current value of the {@link Receiver} auto settlement setting.
+     */
+    public boolean autoSettle() {
+        return autoSettle;
+    }
+
+    /**
+     * Sets the {@link DeliveryMode} value to assign to newly created {@link Receiver} instances.
+     *
+     * @param deliveryMode
+     *      The delivery mode value to configure.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions deliveryMode(DeliveryMode deliveryMode) {
+        this.deliveryMode = deliveryMode;
+        return this;
+    }
+
+    /**
+     * @return the current value of the {@link Receiver} delivery mode configuration.
+     */
+    public DeliveryMode deliveryMode() {
+        return deliveryMode;
+    }
+
+    /**
+     * Configures the link name to use when creating a given {@link Receiver} instance.
+     *
+     * @param linkName
+     *      The assigned link name to use when creating a {@link Receiver}.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions linkName(String linkName) {
+        this.linkName = linkName;
+        return this;
+    }
+
+    /**
+     * @return the configured link name to use when creating a {@link Receiver}.
+     */
+    public String linkName() {
+        return linkName;
+    }
+
+    /**
+     * @return the credit window configuration that will be applied to created {@link Receiver} instances.
+     */
+    public int creditWindow() {
+        return creditWindow;
+    }
+
+    /**
+     * A credit window value that will be used to maintain an window of credit for Receiver instances
+     * that are created.  The {@link Receiver} will allow up to the credit window amount of incoming
+     * deliveries to be queued and as they are read from the {@link Receiver} the window will be extended
+     * to maintain a consistent backlog of deliveries.  The default is to configure a credit window of 10.
+     * <p>
+     * To disable credit windowing and allow the client application to control the credit on the {@link Receiver}
+     * link the credit window value should be set to zero.
+     *
+     * @param creditWindow
+     *      The assigned credit window value to use.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions creditWindow(int creditWindow) {
+        this.creditWindow = creditWindow;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a {@link Receiver} is closed.
+     */
+    public long closeTimeout() {
+        return closeTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to close
+     * the {@link Receiver} link.
+     *
+     * @param closeTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions closeTimeout(long closeTimeout) {
+        this.closeTimeout = closeTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a {@link Receiver} is opened.
+     */
+    public long openTimeout() {
+        return openTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to open
+     * a {@link Receiver} has been honored.
+     *
+     * @param openTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions openTimeout(long openTimeout) {
+        this.openTimeout = openTimeout;
+        return this;
+    }
+
+    /**
+     * @return the configured drain timeout value that will use to fail a pending drain request.
+     */
+    public long drainTimeout() {
+        return drainTimeout;
+    }
+
+    /**
+     * Sets the drain timeout (in milliseconds) after which a {@link Receiver} request to drain
+     * link credit is considered failed and the request will be marked as such.
+     *
+     * @param drainTimeout
+     *      the drainTimeout to use for receiver links.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions drainTimeout(long drainTimeout) {
+        this.drainTimeout = drainTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource makes a request.
+     */
+    public long requestTimeout() {
+        return requestTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to
+     * perform some action such as starting a new transaction.  If the remote does not respond
+     * within the configured timeout the resource making the request will mark it as failed and
+     * return an error to the request initiator usually in the form of a
+     * {@link ClientOperationTimedOutException}.
+     *
+     * @param requestTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions requestTimeout(long requestTimeout) {
+        this.requestTimeout = requestTimeout;
+        return this;
+    }
+
+    /**
+     * @return the offeredCapabilities
+     */
+    public String[] offeredCapabilities() {
+        return offeredCapabilities;
+    }
+
+    /**
+     * @param offeredCapabilities the offeredCapabilities to set
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions offeredCapabilities(String... offeredCapabilities) {
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the desiredCapabilities
+     */
+    public String[] desiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    /**
+     * @param desiredCapabilities the desiredCapabilities to set
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions desiredCapabilities(String... desiredCapabilities) {
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the properties
+     */
+    public Map<String, Object> properties() {
+        return properties;
+    }
+
+    /**
+     * @param properties the properties to set
+     *
+     * @return this {@link ReceiverOptions} instance.
+     */
+    public ReceiverOptions properties(Map<String, Object> properties) {
+        this.properties = properties;
+        return this;
+    }
+
+    /**
+     * @return the source options that will be used when creating new {@link Receiver} instances.
+     */
+    public SourceOptions sourceOptions() {
+        return source;
+    }
+
+    /**
+     * @return the target options that will be used when creating new {@link Sender} instances.
+     */
+    public TargetOptions targetOptions() {
+        return target;
+    }
+
+    @Override
+    public ReceiverOptions clone() {
+        return copyInto(new ReceiverOptions());
+    }
+
+    /**
+     * Copy all options from this {@link ReceiverOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this options class for chaining.
+     */
+    protected ReceiverOptions copyInto(ReceiverOptions other) {
+        other.creditWindow(creditWindow);
+        other.linkName(linkName);
+        other.closeTimeout(closeTimeout);
+        other.openTimeout(openTimeout);
+        other.drainTimeout(drainTimeout);
+        other.requestTimeout(requestTimeout);
+
+        if (offeredCapabilities != null) {
+            other.offeredCapabilities(Arrays.copyOf(offeredCapabilities, offeredCapabilities.length));
+        }
+        if (desiredCapabilities != null) {
+            other.desiredCapabilities(Arrays.copyOf(desiredCapabilities, desiredCapabilities.length));
+        }
+        if (properties != null) {
+            other.properties(new HashMap<>(properties));
+        }
+
+        source.copyInto(other.sourceOptions());
+        target.copyInto(other.targetOptions());
+
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ReconnectOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ReconnectOptions.java
new file mode 100644
index 0000000..fcb0af2
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/ReconnectOptions.java
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Options that control the reconnection behavior of a client {@link Connection}.
+ */
+public class ReconnectOptions {
+
+    public static final boolean DEFAULT_RECONNECT_ENABLED = false;
+    public static final int INFINITE = -1;
+    public static final int DEFAULT_WARN_AFTER_RECONNECT_ATTEMPTS = 10;
+    public static final int DEFAULT_RECONNECT_DEALY = 10;
+    public static final int DEFAULT_MAX_RECONNECT_DEALY = 30_000;
+    public static final boolean DEFAULT_USE_RECONNECT_BACKOFF = true;
+    public static final double DEFAULT_RECONNECT_BACKOFF_MULTIPLIER = 2.0d;
+
+    private final List<URI> reconnectHosts = new ArrayList<>();
+
+    private boolean reconnectEnabled = DEFAULT_RECONNECT_ENABLED;
+    private int warnAfterReconnectAttempts = DEFAULT_WARN_AFTER_RECONNECT_ATTEMPTS;
+    private int maxInitialConnectionAttempts = INFINITE;
+    private int maxReconnectAttempts = INFINITE;
+    private int reconnectDelay = DEFAULT_RECONNECT_DEALY;
+    private int maxReconnectDelay = DEFAULT_MAX_RECONNECT_DEALY;
+    private boolean useReconnectBackOff = DEFAULT_USE_RECONNECT_BACKOFF;
+    private double reconnectBackOffMultiplier = DEFAULT_RECONNECT_BACKOFF_MULTIPLIER;
+
+    /**
+     * Create a new {@link ConnectionOptions} instance configured with default configuration settings.
+     */
+    public ReconnectOptions() {
+    }
+
+    /**
+     * Creates a {@link ReconnectOptions} instance that is a copy of the given instance.
+     *
+     * @param options
+     *      The {@link ReconnectOptions} instance whose configuration should be copied to this one.
+     */
+    public ReconnectOptions(ReconnectOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    /**
+     * Copy all options from this {@link ReconnectOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    protected ReconnectOptions copyInto(ReconnectOptions other) {
+        other.reconnectEnabled(reconnectEnabled());
+        other.warnAfterReconnectAttempts(warnAfterReconnectAttempts);
+        other.maxInitialConnectionAttempts(maxInitialConnectionAttempts);
+        other.maxReconnectAttempts(maxReconnectAttempts);
+        other.reconnectDelay(reconnectDelay);
+        other.maxReconnectDelay(maxReconnectDelay);
+        other.useReconnectBackOff(useReconnectBackOff);
+        other.reconnectBackOffMultiplier(reconnectBackOffMultiplier);
+        other.reconnectHosts.addAll(reconnectHosts);
+
+        return this;
+    }
+
+    /**
+     * Returns <code>true</code> if reconnect is currently enabled for the {@link Connection} that
+     * these options are assigned to.
+     *
+     * @return the reconnect enabled configuration state for this options instance.
+     */
+    public boolean reconnectEnabled() {
+        return reconnectEnabled;
+    }
+
+    /**
+     * Set to <code>true</code> to enable reconnection support on the associated {@link Connection}
+     * or <code>false</code> to disable.  When enabled a {@link Connection} will attempt to reconnect
+     * to a remote based on the configuration set in this options instance.
+     *
+     * @param reconnectEnabled
+     *      Controls if reconnection is enabled or not for the associated {@link Connection}.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions reconnectEnabled(boolean reconnectEnabled) {
+        this.reconnectEnabled = reconnectEnabled;
+        return this;
+    }
+
+    /**
+     * Adds an additional host location that can be used when attempting to reconnect the client
+     * following a connection failure.
+     *
+     * @param reconnectHost
+     *      The host name of the remote host to attempt a reconnection to.
+     * @param port
+     *      The port on the remote host to use when connecting.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions addReconnectHost(String reconnectHost, int port) {
+        try {
+            reconnectHosts.add(new URI(null, null, reconnectHost, port, null, null, null));
+        } catch (URISyntaxException e) {
+        }
+        return this;
+    }
+
+    /**
+     * @return an unmodifiable view of the configured reconnect hosts.
+     */
+    public List<URI> reconnectHosts() {
+        return Collections.unmodifiableList(reconnectHosts);
+    }
+
+    /**
+     * @return the number of reconnection attempt before the client should log a warning.
+     */
+    public int warnAfterReconnectAttempts() {
+        return warnAfterReconnectAttempts;
+    }
+
+    /**
+     * Controls how often the client will log a message indicating that a reconnection is being attempted.
+     * The default is to log every 10 connection attempts.
+     *
+     * @param warnAfterReconnectAttempts
+     *      The number of attempts before logging an update about not yet reconnecting.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions warnAfterReconnectAttempts(int warnAfterReconnectAttempts) {
+        this.warnAfterReconnectAttempts = warnAfterReconnectAttempts;
+        return this;
+    }
+
+    /**
+     * @return the configured maximum number of initial connection attempts to try before giving up
+     */
+    public int maxInitialConnectionAttempts() {
+        return maxInitialConnectionAttempts;
+    }
+
+    /**
+     * For a client that has never connected to a remote peer before this option controls how many attempts
+     * are made to connect before reporting the connection as failed. The default behavior is to use the
+     * value of maxReconnectAttempts.
+     *
+     * @param maxInitialConnectionAttempts
+     *      the maximum number of initial connection attempts to try before giving up.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions maxInitialConnectionAttempts(int maxInitialConnectionAttempts) {
+        this.maxInitialConnectionAttempts = maxInitialConnectionAttempts;
+        return this;
+    }
+
+    /**
+     * @return the configured maximum number of reconnection attempts to try before giving up
+     */
+    public int maxReconnectAttempts() {
+        return maxReconnectAttempts;
+    }
+
+    /**
+     * The number of reconnection attempts allowed before reporting the connection as failed to the client.
+     * The default is no limit or (-1).
+     *
+     * @param maxReconnectionAttempts
+     *      the maximum number of reconnection attempts to try before giving up.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions maxReconnectAttempts(int maxReconnectionAttempts) {
+        this.maxReconnectAttempts = maxReconnectionAttempts;
+        return this;
+    }
+
+    /**
+     * @return the configured reconnect delay to use after between attempts to connect or reconnect.
+     */
+    public int reconnectDelay() {
+        return reconnectDelay;
+    }
+
+    /**
+     * Controls the delay between successive reconnection attempts, defaults to 10 milliseconds. If the
+     * back off option is not enabled this value remains constant.
+     *
+     * @param reconnectDelay
+     *      The reconnect delay to apply to successive attempts to reconnect.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions reconnectDelay(int reconnectDelay) {
+        this.reconnectDelay = reconnectDelay;
+        return this;
+    }
+
+    /**
+     * @return the configured maximum reconnect attempt delay allowed when using delay back off scheduling.
+     */
+    public int maxReconnectDelay() {
+        return maxReconnectDelay;
+    }
+
+    /**
+     * The maximum time that the client will wait before attempting a reconnect. This value is only used when the
+     * back off feature is enabled to ensure that the delay does not grow too large. Defaults to 30 seconds as the
+     * max time between successive connection attempts.
+     *
+     * @param maxReconnectDelay
+     *      The maximum interval allowed when connection attempt back off is in effect.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions maxReconnectDelay(int maxReconnectDelay) {
+        this.maxReconnectDelay = maxReconnectDelay;
+        return this;
+    }
+
+    /**
+     * @return if the reconnection attempts will be delayed using a back off multiplier.
+     */
+    public boolean useReconnectBackOff() {
+        return useReconnectBackOff;
+    }
+
+    /**
+     * Controls whether the time between reconnection attempts should grow based on a configured multiplier.
+     * This option defaults to true.
+     *
+     * @param useReconnectBackOff
+     *      should connection attempts use a back off of the configured delay.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions useReconnectBackOff(boolean useReconnectBackOff) {
+        this.useReconnectBackOff = useReconnectBackOff;
+        return this;
+    }
+
+    /**
+     * @return the multiplier used when the reconnection back off feature is enabled.
+     */
+    public double reconnectBackOffMultiplier() {
+        return reconnectBackOffMultiplier;
+    }
+
+    /**
+     * The multiplier used to grow the reconnection delay value, defaults to 2.0d.
+     *
+     * @param reconnectBackOffMultiplier
+     *      the delay multiplier used when building delay between reconnection attempts.
+     *
+     * @return this {@link ReconnectOptions} instance.
+     */
+    public ReconnectOptions reconnectBackOffMultiplier(double reconnectBackOffMultiplier) {
+        this.reconnectBackOffMultiplier = reconnectBackOffMultiplier;
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SaslOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SaslOptions.java
new file mode 100644
index 0000000..3dfb55c
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SaslOptions.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Connection options that are applied to the SASL layer.
+ */
+public class SaslOptions {
+
+    public static final boolean DEFAULT_SASL_ENABLED = true;
+
+    private boolean saslEnabled = DEFAULT_SASL_ENABLED;
+    private final Set<String> saslAllowedMechs = new LinkedHashSet<>();
+
+    public SaslOptions() {
+    }
+
+    @Override
+    public SaslOptions clone() {
+        return copyInto(new SaslOptions());
+    }
+
+    /**
+     * @return the saslLayer
+     */
+    public boolean saslEnabled() {
+        return saslEnabled;
+    }
+
+    /**
+     * @param saslEnabled the saslLayer to set
+     *
+     * @return this options instance.
+     */
+    public SaslOptions saslEnabled(boolean saslEnabled) {
+        this.saslEnabled = saslEnabled;
+        return this;
+    }
+
+    /**
+     * Adds a mechanism to the list of allowed SASL mechanisms this client will use
+     * when selecting from the remote peers offered set of SASL mechanisms.  If no
+     * allowed mechanisms are configured then the client will select the first mechanism
+     * from the server offered mechanisms that is supported.
+     *
+     * @param mechanism
+     * 		The mechanism to allow.
+     *
+     * @return this options object for chaining.
+     */
+    public SaslOptions addAllowedMechanism(String mechanism) {
+        Objects.requireNonNull(mechanism, "Cannot add null as an allowed mechanism");
+        this.saslAllowedMechs.add(mechanism);
+        return this;
+    }
+
+    /**
+     * @return the current list of allowed SASL Mechanisms.
+     */
+    public Set<String> allowedMechanisms() {
+        return Collections.unmodifiableSet(saslAllowedMechs);
+    }
+
+    /**
+     * Copy all configuration into the given {@link SaslOptions} from this instance.
+     *
+     * @param other
+     * 		another {@link SaslOptions} instance that will receive the configuration from this instance.
+     *
+     * @return the options instance that was copied into.
+     */
+    public SaslOptions copyInto(SaslOptions other) {
+        other.saslEnabled(saslEnabled());
+        other.saslAllowedMechs.addAll(saslAllowedMechs);
+
+        return other;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Sender.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Sender.java
new file mode 100644
index 0000000..034743d
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Sender.java
@@ -0,0 +1,256 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * AMQP Sender that provides an API for sending complete Message payload data.
+ */
+public interface Sender extends AutoCloseable {
+
+    /**
+     * @return a {@link Future} that will be completed when the remote opens this {@link Sender}.
+     */
+    Future<Sender> openFuture();
+
+    /**
+     * Requests a close of the {@link Sender} at the remote and waits until the Sender has been
+     * fully closed or until the configured {@link SenderOptions#closeTimeout()} is exceeded.
+     */
+    @Override
+    void close();
+
+    /**
+     * Requests a close of the {@link Sender} at the remote and waits until the Sender has been
+     * fully closed or until the configured {@link SenderOptions#closeTimeout()} is exceeded.
+     *
+     * @param error
+     *      The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     */
+    void close(ErrorCondition error);
+
+    /**
+     * Requests a detach of the {@link Sender} at the remote and waits until the Sender has been
+     * fully detached or until the configured {@link SenderOptions#closeTimeout()} is exceeded.
+     */
+    void detach();
+
+    /**
+     * Requests a detach of the {@link Sender} at the remote and waits until the Sender has been
+     * fully detached or until the configured {@link SenderOptions#closeTimeout()} is exceeded.
+     *
+     * @param error
+     *      The {@link ErrorCondition} to transmit to the remote along with the detach operation.
+     */
+    void detach(ErrorCondition error);
+
+    /**
+     * Requests a close of the {@link Sender} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been closed.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Sender} link.
+     */
+    Future<Sender> closeAsync();
+
+    /**
+     * Requests a close of the {@link Sender} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been closed.
+     *
+     * @param error
+     * 		The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Sender} link.
+     */
+    Future<Sender> closeAsync(ErrorCondition error);
+
+    /**
+     * Requests a detach of the {@link Sender} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been detached.
+     *
+     * @return a {@link Future} that will be completed when the remote detaches this {@link Sender} link.
+     */
+    Future<Sender> detachAsync();
+
+    /**
+     * Requests a detach of the {@link Sender} link at the remote and returns a {@link Future} that will be
+     * completed once the link has been detached.
+     *
+     * @param error
+     * 		The {@link ErrorCondition} to transmit to the remote along with the detach operation.
+     *
+     * @return a {@link Future} that will be completed when the remote detaches this {@link Sender} link.
+     */
+    Future<Sender> detachAsync(ErrorCondition error);
+
+    /**
+     * Returns the address that the {@link Sender} instance will send {@link Message} objects
+     * to.  The value returned from this method is control by the configuration that was used
+     * to create the sender.
+     *
+     * <ul>
+     *  <li>
+     *    If the Sender is configured as an anonymous sender then this method returns null.
+     *  </li>
+     *  <li>
+     *    If the Sender was created with the dynamic sender methods then the method will return
+     *    the dynamically created address once the remote has attached its end of the sender link.
+     *    Due to the need to await the remote peer to populate the dynamic address this method will
+     *    block until the open of the sender link has completed.
+     *  </li>
+     *  <li>
+     *    If neither of the above is true then the address returned is the address passed to the original
+     *    {@link Session#openSender(String)} or {@link Session#openSender(String, SenderOptions)} methods.
+     *  </li>
+     * </ul>
+     *
+     * @return the address that this {@link Sender} is sending to.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Sender} address.
+     */
+    String address() throws ClientException;
+
+    /**
+     * Returns an immutable view of the remote {@link Source} object assigned to this sender link.  If the
+     * attach has not completed yet this method will block to await the attach response which carries the remote
+     * {@link Source}.
+     *
+     * @return the remote {@link Source} node configuration.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Sender} remote {@link Source}.
+     */
+    Source source() throws ClientException;
+
+    /**
+     * Returns an immutable view of the remote {@link Target} object assigned to this sender link.  If the
+     * attach has not completed yet this method will block to await the attach response which carries the remote
+     * {@link Target}.
+     *
+     * @return the remote {@link Target} node configuration.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Sender} remote {@link Target}.
+     */
+    Target target() throws ClientException;
+
+    /**
+     * Returns the properties that the remote provided upon successfully opening the {@link Sender}.  If the
+     * attach has not completed yet this method will block to await the attach response which carries the remote
+     * properties.  If the remote provides no properties this method will return null.
+     *
+     * @return any properties provided from the remote once the sender has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Sender} remote properties.
+     */
+    Map<String, Object> properties() throws ClientException;
+
+    /**
+     * Returns the offered capabilities that the remote provided upon successfully opening the {@link Sender}.
+     * If the attach has not completed yet this method will block to await the attach response which carries the
+     * remote offered capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any capabilities provided from the remote once the sender has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Sender} remote offered capabilities.
+     */
+    String[] offeredCapabilities() throws ClientException;
+
+    /**
+     * Returns the desired capabilities that the remote provided upon successfully opening the {@link Sender}.
+     * If the attach has not completed yet this method will block to await the attach response which carries the
+     * remote desired capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any desired capabilities provided from the remote once the sender has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Sender} remote desired capabilities.
+     */
+    String[] desiredCapabilities() throws ClientException;
+
+    /**
+     * @return the {@link Client} instance that holds this session's {@link Sender}
+     */
+    Client client();
+
+    /**
+     * @return the {@link Connection} instance that holds this session's {@link Sender}
+     */
+    Connection connection();
+
+    /**
+     * @return the {@link Session} that created and holds this {@link Sender}.
+     */
+    Session session();
+
+    /**
+     * Send the given message immediately if there is credit available or blocks if the link
+     * has not yet been granted credit.
+     *
+     * @param message
+     *      the {@link Message} to send.
+     *
+     * @return the {@link Tracker} for the message delivery
+     *
+     * @throws ClientException if an error occurs while initiating the send operation.
+     */
+    Tracker send(Message<?> message) throws ClientException;
+
+    /**
+     * Send the given message immediately if there is credit available or blocks if the link
+     * has not yet been granted credit.
+     *
+     * @param message
+     *      the {@link Message} to send.
+     * @param deliveryAnnotations
+     *      the delivery annotations that should be included in the sent {@link Message}.
+     *
+     * @return the {@link Tracker} for the message delivery
+     *
+     * @throws ClientException if an error occurs while initiating the send operation.
+     */
+    Tracker send(Message<?> message, Map<String, Object> deliveryAnnotations) throws ClientException;
+
+    /**
+     * Send the given message if credit is available or returns null if no credit has been
+     * granted to the link at the time of the send attempt.
+     *
+     * @param message
+     *      the {@link Message} to send if credit is available.
+     *
+     * @return the {@link Tracker} for the message delivery or null if no credit for sending.
+     *
+     * @throws ClientException if an error occurs while initiating the send operation.
+     */
+    Tracker trySend(Message<?> message) throws ClientException;
+
+    /**
+     * Send the given message if credit is available or returns null if no credit has been
+     * granted to the link at the time of the send attempt.
+     *
+     * @param message
+     *      the {@link Message} to send if credit is available.
+     * @param deliveryAnnotations
+     *      the delivery annotations that should be included in the sent {@link Message}.
+     *
+     * @return the {@link Tracker} for the message delivery or null if no credit for sending.
+     *
+     * @throws ClientException if an error occurs while initiating the send operation.
+     */
+    Tracker trySend(Message<?> message, Map<String, Object> deliveryAnnotations) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SenderOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SenderOptions.java
new file mode 100644
index 0000000..d862a37
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SenderOptions.java
@@ -0,0 +1,316 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientSendTimedOutException;
+
+/**
+ * Options that control the behavior of a {@link Sender} created from them.
+ */
+public class SenderOptions {
+
+    private long sendTimeout = ConnectionOptions.DEFAULT_SEND_TIMEOUT;
+    private long requestTimeout = ConnectionOptions.DEFAULT_REQUEST_TIMEOUT;
+    private long openTimeout = ConnectionOptions.DEFAULT_OPEN_TIMEOUT;
+    private long closeTimeout = ConnectionOptions.DEFAULT_CLOSE_TIMEOUT;
+
+    private String linkName;
+    private boolean autoSettle = true;
+    private DeliveryMode deliveryMode = DeliveryMode.AT_LEAST_ONCE;
+
+    private final SourceOptions source = new SourceOptions();
+    private final TargetOptions target = new TargetOptions();
+
+    private String[] offeredCapabilities;
+    private String[] desiredCapabilities;
+    private Map<String, Object> properties;
+
+    public SenderOptions() {
+    }
+
+    public SenderOptions(SenderOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    /**
+     * Configures the link name to use when creating a given {@link Sender} instance.
+     *
+     * @param linkName
+     *      The assigned link name to use when creating a {@link Sender}.
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions linkName(String linkName) {
+        this.linkName = linkName;
+        return this;
+    }
+
+    /**
+     * @return the configured link name to use when creating a {@link Sender}.
+     */
+    public String linkName() {
+        return linkName;
+    }
+
+    /**
+     * Sets whether sent deliveries should be automatically locally-settled once
+     * they have become remotely-settled by the receiving peer.
+     *
+     * True by default.
+     *
+     * @param autoSettle
+     *            whether deliveries should be auto settled locally after being
+     *            settled by the receiver
+     *
+     * @return the sender
+     */
+    public SenderOptions autoSettle(boolean autoSettle) {
+        this.autoSettle = autoSettle;
+        return this;
+    }
+
+    /**
+     * Get whether the {@link Sender} is auto settling deliveries.
+     *
+     * @return whether deliveries should be auto settled locally after being settled
+     *         by the receiver
+     *
+     * @see #autoSettle(boolean)
+     */
+    public boolean autoSettle() {
+        return autoSettle;
+    }
+
+    /**
+     * Sets the {@link DeliveryMode} value to assign to newly created {@link Sender} instances.
+     *
+     * @param deliveryMode
+     *      The delivery mode value to configure.
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions deliveryMode(DeliveryMode deliveryMode) {
+        this.deliveryMode = deliveryMode;
+        return this;
+    }
+
+    /**
+     * @return the current value of the {@link Sender} delivery mode configuration.
+     */
+    public DeliveryMode deliveryMode() {
+        return deliveryMode;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a {@link Sender} is closed.
+     */
+    public long closeTimeout() {
+        return closeTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to close
+     * the {@link Sender} link.
+     *
+     * @param closeTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions closeTimeout(long closeTimeout) {
+        this.closeTimeout = closeTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a {@link Sender} is opened.
+     */
+    public long openTimeout() {
+        return openTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to open
+     * a {@link Sender} has been honored.
+     *
+     * @param openTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions openTimeout(long openTimeout) {
+        this.openTimeout = openTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is message send.
+     */
+    public long sendTimeout() {
+        return sendTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a send operation to complete.  A send will block if the
+     * remote has not granted the {@link Sender} or the {@link Session} credit to do so, if the send blocks
+     * for longer than this timeout the send call will fail with an {@link ClientSendTimedOutException}
+     * exception to indicate that the send did not complete.
+     *
+     * @param sendTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions sendTimeout(long sendTimeout) {
+        this.sendTimeout = sendTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource makes a request.
+     */
+    public long requestTimeout() {
+        return requestTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to
+     * perform some action such as starting a new transaction.  If the remote does not respond
+     * within the configured timeout the resource making the request will mark it as failed and
+     * return an error to the request initiator usually in the form of a
+     * {@link ClientOperationTimedOutException}.
+     *
+     * @param requestTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions requestTimeout(long requestTimeout) {
+        this.requestTimeout = requestTimeout;
+        return this;
+    }
+
+    /**
+     * @return the offeredCapabilities
+     */
+    public String[] offeredCapabilities() {
+        return offeredCapabilities;
+    }
+
+    /**
+     * @param offeredCapabilities the offeredCapabilities to set
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions offeredCapabilities(String... offeredCapabilities) {
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the desiredCapabilities
+     */
+    public String[] desiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    /**
+     * @param desiredCapabilities the desiredCapabilities to set
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions desiredCapabilities(String... desiredCapabilities) {
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the properties
+     */
+    public Map<String, Object> properties() {
+        return properties;
+    }
+
+    /**
+     * @param properties the properties to set
+     *
+     * @return this {@link SenderOptions} instance.
+     */
+    public SenderOptions properties(Map<String, Object> properties) {
+        this.properties = properties;
+        return this;
+    }
+
+    /**
+     * @return the source
+     */
+    public SourceOptions sourceOptions() {
+        return source;
+    }
+
+    /**
+     * @return the target
+     */
+    public TargetOptions targetOptions() {
+        return target;
+    }
+
+    @Override
+    public SenderOptions clone() {
+        return copyInto(new SenderOptions());
+    }
+
+    /**
+     * Copy all options from this {@link SenderOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this options class for chaining.
+     */
+    protected SenderOptions copyInto(SenderOptions other) {
+        other.autoSettle(autoSettle);
+        other.linkName(linkName);
+        other.closeTimeout(closeTimeout);
+        other.openTimeout(openTimeout);
+        other.sendTimeout(sendTimeout);
+        other.requestTimeout(requestTimeout);
+
+        if (offeredCapabilities != null) {
+            other.offeredCapabilities(Arrays.copyOf(offeredCapabilities, offeredCapabilities.length));
+        }
+        if (desiredCapabilities != null) {
+            other.desiredCapabilities(Arrays.copyOf(desiredCapabilities, desiredCapabilities.length));
+        }
+        if (properties != null) {
+            other.properties(new HashMap<>(properties));
+        }
+
+        source.copyInto(other.sourceOptions());
+        target.copyInto(other.targetOptions());
+
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Session.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Session.java
new file mode 100644
index 0000000..18260da
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Session.java
@@ -0,0 +1,329 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionNotActiveException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+
+/**
+ * Session object used to create {@link Sender} and {@link Receiver} instances.
+ */
+public interface Session extends AutoCloseable {
+
+    /**
+     * @return the {@link Client} instance that holds this session's {@link Connection}
+     */
+    Client client();
+
+    /**
+     * @return the {@link Connection} that created and holds this {@link Session}.
+     */
+    Connection connection();
+
+    /**
+     * @return a {@link Future} that will be completed when the remote opens this {@link Session}.
+     */
+    Future<Session> openFuture();
+
+    /**
+     * Requests a close of the {@link Session} at the remote and waits until the Session has been
+     * fully closed or until the configured {@link SessionOptions#closeTimeout()} is exceeded.
+     */
+    @Override
+    void close();
+
+    /**
+     * Requests a close of the {@link Session} at the remote and waits until the Session has been
+     * fully closed or until the configured {@link SessionOptions#closeTimeout()} is exceeded.
+     *
+     * @param error
+     *      The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     */
+    void close(ErrorCondition error);
+
+    /**
+     * Requests a close of the {@link Session} at the remote and returns a {@link Future} that will be
+     * completed once the session has been remotely closed or an error occurs.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Session}.
+     */
+    Future<Session> closeAsync();
+
+    /**
+     * Requests a close of the {@link Session} at the remote and returns a {@link Future} that will be
+     * completed once the session has been remotely closed or an error occurs.
+     *
+     * @param error
+     * 		The {@link ErrorCondition} to transmit to the remote along with the close operation.
+     *
+     * @return a {@link Future} that will be completed when the remote closes this {@link Session}.
+     */
+    Future<Session> closeAsync(ErrorCondition error);
+
+    /**
+     * Creates a receiver used to consume messages from the given node address.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openReceiver(String address) throws ClientException;
+
+    /**
+     * Creates a receiver used to consume messages from the given node address.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     * @param receiverOptions
+     *            The options for this receiver.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openReceiver(String address, ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a receiver used to consume messages from the given node address and configure it
+     * such that the remote create a durable node.
+     *
+     * @param address
+     * 			The source address to attach the consumer to.
+     * @param subscriptionName
+     * 			The name to give the subscription (link name).
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDurableReceiver(String address, String subscriptionName) throws ClientException;
+
+    /**
+     * Creates a receiver used to consume messages from the given node address and configure it
+     * such that the remote create a durable node.
+     *
+     * @param address
+     *            The source address to attach the consumer to.
+     * @param subscriptionName
+     * 			The name to give the subscription (link name).
+     * @param receiverOptions
+     *            The options for this receiver.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDurableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver() throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * @param dynamicNodeProperties
+     * 		The dynamic node properties to be applied to the node created by the remote.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties) throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * @param receiverOptions
+     * 		The options for this receiver.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver(ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a dynamic receiver used to consume messages from the given node address.
+     *
+     * @param dynamicNodeProperties
+     * 		The dynamic node properties to be applied to the node created by the remote.
+     * @param receiverOptions
+     *      The options for this receiver.
+     *
+     * @return the newly created {@link Receiver}
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException;
+
+    /**
+     * Creates a sender used to send messages to the given node address. If no
+     * address (i.e null) is specified then a sender will be established to the
+     * 'anonymous relay' and each message must specify its destination address.
+     *
+     * @param address
+     *            The target address to attach to, or null to attach to the
+     *            anonymous relay.
+     *
+     * @return the newly created {@link Sender}.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Sender openSender(String address) throws ClientException;
+
+    /**
+     * Creates a sender used to send messages to the given node address. If no
+     * address (i.e null) is specified then a sender will be established to the
+     * 'anonymous relay' and each message must specify its destination address.
+     *
+     * @param address
+     *            The target address to attach to, or null to attach to the
+     *            anonymous relay.
+     * @param senderOptions
+     *            The options for this sender.
+     *
+     * @return the newly created {@link Sender}.
+     *
+     * @throws ClientException if an internal error occurs.
+     */
+    Sender openSender(String address, SenderOptions senderOptions) throws ClientException;
+
+    /**
+     * Creates a sender that is established to the 'anonymous relay' and as such each
+     * message that is sent using this sender must specify an address in its destination
+     * address field.
+     *
+     * @return the newly created {@link Sender}.
+     *
+     * @throws ClientException if an internal error occurs.
+     * @throws ClientUnsupportedOperationException if the remote did not signal support for anonymous relays.
+     */
+    Sender openAnonymousSender() throws ClientException;
+
+    /**
+     * Creates a sender that is established to the 'anonymous relay' and as such each
+     * message that is sent using this sender must specify an address in its destination
+     * address field.
+     *
+     * @param senderOptions
+     *            The options for this sender.
+     *
+     * @return the newly created {@link Sender}.
+     *
+     * @throws ClientException if an internal error occurs.
+     * @throws ClientUnsupportedOperationException if the remote did not signal support for anonymous relays.
+     */
+    Sender openAnonymousSender(SenderOptions senderOptions) throws ClientException;
+
+    /**
+     * Returns the properties that the remote provided upon successfully opening the {@link Session}.  If the
+     * open has not completed yet this method will block to await the open response which carries the remote
+     * properties.  If the remote provides no properties this method will return null.
+     *
+     * @return any properties provided from the remote once the session has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Session} remote properties.
+     */
+    Map<String, Object> properties() throws ClientException;
+
+    /**
+     * Returns the offered capabilities that the remote provided upon successfully opening the {@link Session}.
+     * If the open has not completed yet this method will block to await the open response which carries the
+     * remote offered capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any capabilities provided from the remote once the session has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Session} remote offered capabilities.
+     */
+    String[] offeredCapabilities() throws ClientException;
+
+    /**
+     * Returns the desired capabilities that the remote provided upon successfully opening the {@link Session}.
+     * If the open has not completed yet this method will block to await the open response which carries the
+     * remote desired capabilities.  If the remote provides no capabilities this method will return null.
+     *
+     * @return any desired capabilities provided from the remote once the session has successfully opened.
+     *
+     * @throws ClientException if an error occurs while obtaining the {@link Session} remote desired capabilities.
+     */
+    String[] desiredCapabilities() throws ClientException;
+
+    /**
+     * Opens a new transaction scoped to this {@link Session} if one is not already active.
+     *
+     * A {@link Session} that has an active transaction will perform all sends and all delivery dispositions
+     * under that active transaction.  If the user wishes to send with the same session but outside of a
+     * transaction they user must commit the active transaction and not request that a new one be started.
+     * A session can only have one active transaction at a time and as such any call to begin while there is
+     * a currently active transaction will throw an {@link ClientTransactionNotActiveException} to indicate that
+     * the operation being requested is not valid at that time.
+     *
+     * This is a blocking method that will return successfully only after a new transaction has been started.
+     *
+     * @return this {@link Session} instance.
+     *
+     * @throws ClientException if an error occurs while attempting to begin a new transaction.
+     */
+    Session beginTransaction() throws ClientException;
+
+    /**
+     * Commit the currently active transaction in this Session.
+     *
+     * Commit the currently active transaction in this Session but does not start a new transaction
+     * automatically.  If there is no current transaction this method will throw an {@link ClientTransactionNotActiveException}
+     * to indicate this error.  If the active transaction has entered an in doubt state or was remotely rolled
+     * back this method will throw an error to indicate that the commit failed and that a new transaction
+     * need to be started by the user.  When a transaction rolled back error occurs the user should assume that
+     * all work performed under that transaction has failed and will need to be attempted under a new transaction.
+     *
+     * This is a blocking method that will return successfully only after the current transaction has been committed.
+     *
+     * @return this {@link Session} instance.
+     *
+     * @throws ClientException if an error occurs while attempting to commit the current transaction.
+     */
+    Session commitTransaction() throws ClientException;
+
+    /**
+     * Roll back the currently active transaction in this Session.
+     *
+     * Roll back the currently active transaction in this Session but does not automatically start a new
+     * transaction.  If there is no current transaction this method will throw an {@link ClientTransactionNotActiveException}
+     * to indicate this error.  If the active transaction has entered an in doubt state or was remotely rolled
+     * back this method will throw an error to indicate that the roll back failed and that a new transaction need
+     * to be started by the user.
+     *
+     * @return this {@link Session} instance.
+     *
+     * @throws ClientException if an error occurs while attempting to roll back the current transaction.
+     */
+    Session rollbackTransaction() throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SessionOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SessionOptions.java
new file mode 100644
index 0000000..47c1798
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SessionOptions.java
@@ -0,0 +1,304 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientSendTimedOutException;
+
+/**
+ * Options that control the behaviour of the {@link Session} created from them.
+ */
+public class SessionOptions {
+
+    public static final int DEFAULT_SESSION_INCOMING_CAPACITY = 100 * 1024 * 1024;
+    public static final int DEFAULT_SESSION_OUTGOING_CAPACITY = 100 * 1024 * 1024;
+
+    private long sendTimeout = ConnectionOptions.DEFAULT_SEND_TIMEOUT;
+    private long drainTimeout = ConnectionOptions.DEFAULT_DRAIN_TIMEOUT;
+    private long requestTimeout = ConnectionOptions.DEFAULT_REQUEST_TIMEOUT;
+    private long openTimeout = ConnectionOptions.DEFAULT_OPEN_TIMEOUT;
+    private long closeTimeout = ConnectionOptions.DEFAULT_CLOSE_TIMEOUT;
+
+    private int incomingCapacity = DEFAULT_SESSION_INCOMING_CAPACITY;
+    private int outgoingCapacity = DEFAULT_SESSION_OUTGOING_CAPACITY;
+
+    private String[] offeredCapabilities;
+    private String[] desiredCapabilities;
+    private Map<String, Object> properties;
+
+    public SessionOptions() {
+    }
+
+    public SessionOptions(SessionOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    @Override
+    public SessionOptions clone() {
+        return copyInto(new SessionOptions());
+    }
+
+    /**
+     * Copy all options from this {@link SessionOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this options class for chaining.
+     */
+    protected SessionOptions copyInto(SessionOptions other) {
+        other.closeTimeout(closeTimeout);
+        other.openTimeout(openTimeout);
+        other.sendTimeout(sendTimeout);
+        other.drainTimeout(drainTimeout);
+        other.requestTimeout(requestTimeout);
+        other.incomingCapacity(incomingCapacity);
+        other.outgoingCapacity(outgoingCapacity);
+
+        if (offeredCapabilities != null) {
+            other.offeredCapabilities(Arrays.copyOf(offeredCapabilities, offeredCapabilities.length));
+        }
+        if (desiredCapabilities != null) {
+            other.desiredCapabilities(Arrays.copyOf(desiredCapabilities, desiredCapabilities.length));
+        }
+        if (properties != null) {
+            other.properties(new HashMap<>(properties));
+        }
+
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is closed.
+     */
+    public long closeTimeout() {
+        return closeTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to close
+     * a {@link Session} as been honored.
+     *
+     * @param closeTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions closeTimeout(long closeTimeout) {
+        this.closeTimeout = closeTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is opened.
+     */
+    public long openTimeout() {
+        return openTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to open
+     * a {@link Session} has been honored.
+     *
+     * @param openTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions openTimeout(long openTimeout) {
+        this.openTimeout = openTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource is message send.
+     */
+    public long sendTimeout() {
+        return sendTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a send operation to complete.  A send will block if the
+     * remote has not granted the {@link Sender} or the {@link Session} credit to do so, if the send blocks
+     * for longer than this timeout the send call will fail with an {@link ClientSendTimedOutException}
+     * exception to indicate that the send did not complete.
+     *
+     * @param sendTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions sendTimeout(long sendTimeout) {
+        this.sendTimeout = sendTimeout;
+        return this;
+    }
+
+    /**
+     * @return the timeout used when awaiting a response from the remote when a resource makes a request.
+     */
+    public long requestTimeout() {
+        return requestTimeout;
+    }
+
+    /**
+     * Configures the timeout used when awaiting a response from the remote that a request to
+     * perform some action such as starting a new transaction.  If the remote does not respond
+     * within the configured timeout the resource making the request will mark it as failed and
+     * return an error to the request initiator usually in the form of a
+     * {@link ClientOperationTimedOutException}.
+     *
+     * @param requestTimeout
+     *      Timeout value in milliseconds to wait for a remote response.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions requestTimeout(long requestTimeout) {
+        this.requestTimeout = requestTimeout;
+        return this;
+    }
+
+    /**
+     * @return the configured drain timeout value that will use to fail a pending drain request.
+     */
+    public long drainTimeout() {
+        return drainTimeout;
+    }
+
+    /**
+     * Sets the drain timeout (in milliseconds) after which a {@link Receiver} request to drain
+     * link credit is considered failed and the request will be marked as such.
+     *
+     * @param drainTimeout
+     *      the drainTimeout to use for receiver links.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions drainTimeout(long drainTimeout) {
+        this.drainTimeout = drainTimeout;
+        return this;
+    }
+
+    /**
+     * @return the offeredCapabilities
+     */
+    public String[] offeredCapabilities() {
+        return offeredCapabilities;
+    }
+
+    /**
+     * @param offeredCapabilities the offeredCapabilities to set
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions offeredCapabilities(String... offeredCapabilities) {
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the desiredCapabilities
+     */
+    public String[] desiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    /**
+     * @param desiredCapabilities the desiredCapabilities to set
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions desiredCapabilities(String... desiredCapabilities) {
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    /**
+     * @return the properties
+     */
+    public Map<String, Object> properties() {
+        return properties;
+    }
+
+    /**
+     * @param properties the properties to set
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions properties(Map<String, Object> properties) {
+        this.properties = properties;
+        return this;
+    }
+
+    /**
+     * @return the incoming capacity that is configured for newly created {@link Session} instances.
+     */
+    public int incomingCapacity() {
+        return incomingCapacity;
+    }
+
+    /**
+     * Sets the incoming capacity for a {@link Session} the created session.  The incoming capacity
+     * control how much buffering a session will allow before applying back pressure to the remote
+     * thereby preventing excessive memory overhead.
+     * <p>
+     * This is an advanced option and in most cases the client defaults should be left in place unless
+     * a specific issue needs to be addressed.
+     *
+     * @param incomingCapacity
+     *      the incoming capacity to set when creating a new {@link Session}.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions incomingCapacity(int incomingCapacity) {
+        this.incomingCapacity = incomingCapacity;
+        return this;
+    }
+
+    /**
+     * @return the outgoing capacity limit that is configured for newly created {@link Session} instances.
+     */
+    public int outgoingCapacity() {
+        return outgoingCapacity;
+    }
+
+    /**
+     * Sets the outgoing capacity for a {@link Session} the created session.  The outgoing capacity
+     * control how much buffering a session will allow before applying back pressure to the local
+     * thereby preventing excessive memory overhead while writing large amounts of data and the
+     * client is experiencing back-pressure due to the remote not keeping pace.
+     * <p>
+     * This is an advanced option and in most cases the client defaults should be left in place unless
+     * a specific issue needs to be addressed.  Setting this value incorrectly can lead to senders that
+     * either block frequently or experience very poor overall performance.
+     *
+     * @param outgoingCapacity
+     *      the outgoing capacity to set when creating a new {@link Session}.
+     *
+     * @return this {@link SessionOptions} instance.
+     */
+    public SessionOptions outgoingCapacity(int outgoingCapacity) {
+        this.outgoingCapacity = outgoingCapacity;
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Source.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Source.java
new file mode 100644
index 0000000..f99e0d0
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Source.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The Source for messages.
+ *
+ * For an opened {@link Sender} or {@link Receiver} the Source properties exposes the
+ * remote {@link Source} configuration.
+ */
+public interface Source {
+
+    /**
+     * @return the address of the Source node.
+     */
+    String address();
+
+    /**
+     * @return the durabilityMode of this Source node.
+     */
+    DurabilityMode durabilityMode();
+
+    /**
+     * @return the timeout assigned to this Source node in seconds.
+     */
+    long timeout();
+
+    /**
+     * @return the {@link ExpiryPolicy} of this Source node.
+     */
+    ExpiryPolicy expiryPolicy();
+
+    /**
+     * @return true if the Source node dynamically on-demand
+     */
+    boolean dynamic();
+
+    /**
+     * @return the properties of the dynamically created Source node.
+     */
+    Map<String, Object> dynamicNodeProperties();
+
+    /**
+     * @return the {@link DistributionMode} of this Source node.
+     */
+    DistributionMode distributionMode();
+
+    /**
+     * @return the filters assigned to this Source node.
+     */
+    Map<String, String> filters();
+
+    /**
+     * @return the default outcome configured for this Source node.
+     */
+    DeliveryState defaultOutcome();
+
+    /**
+     * @return the supported outcome types of this Source node.
+     */
+    Set<DeliveryState.Type> outcomes();
+
+    /**
+     * @return the set of capabilities available on this Source node.
+     */
+    Set<String> capabilities();
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SourceOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SourceOptions.java
new file mode 100644
index 0000000..ddd81fd
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SourceOptions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.impl.ClientDeliveryState;
+
+/**
+ * Options type that carries configuration for link Source types.
+ */
+public final class SourceOptions extends TerminusOptions<SourceOptions> {
+
+    private static final DeliveryState.Type[] DEFAULT_OUTCOMES = new DeliveryState.Type[] {
+        DeliveryState.Type.ACCEPTED, DeliveryState.Type.REJECTED, DeliveryState.Type.RELEASED, DeliveryState.Type.MODIFIED
+    };
+
+    public static final ClientDeliveryState DEFAULT_RECEIVER_OUTCOME = new ClientDeliveryState.ClientModified(true, false);
+
+    private DistributionMode distributionMode;
+    private DeliveryState defaultOutcome;
+    private DeliveryState.Type[] outcomes = DEFAULT_OUTCOMES;
+    private Map<String, String> filters;
+
+    @Override
+    public SourceOptions clone() {
+        return copyInto(new SourceOptions());
+    }
+
+    public SourceOptions copyInto(SourceOptions other) {
+        super.copyInto(other);
+        other.distributionMode(distributionMode);
+        if (filters != null) {
+            other.filters(new HashMap<>(filters));
+        }
+
+        return this;
+    }
+
+    /**
+     * @return the distributionMode
+     */
+    public DistributionMode distributionMode() {
+        return distributionMode;
+    }
+
+    /**
+     * @param distributionMode the distributionMode to set
+     *
+     * @return this {@link SourceOptions} instance.
+     */
+    public SourceOptions distributionMode(DistributionMode distributionMode) {
+        this.distributionMode = distributionMode;
+        return self();
+    }
+
+    /**
+     * @return the filters
+     */
+    public Map<String, String> filters() {
+        return filters;
+    }
+
+    /**
+     * @param filters the filters to set
+     *
+     * @return this {@link SourceOptions} instance.
+     */
+    public SourceOptions filters(Map<String, String> filters) {
+        this.filters = filters;
+        return self();
+    }
+
+    /**
+     * @return the configured default outcome as a {@link DeliveryState} instance.
+     */
+    public DeliveryState defaultOutcome() {
+        return defaultOutcome;
+    }
+
+    /**
+     * @param defaultOutcome
+     * 		The default outcome to assign to the created link source.
+     *
+     * @return this {@link SourceOptions} instance.
+     */
+    public SourceOptions defaultOutcome(DeliveryState defaultOutcome) {
+        this.defaultOutcome = defaultOutcome;
+        return self();
+    }
+
+    /**
+     * @return the currently configured supported outcomes to be used on the create link.
+     */
+    public DeliveryState.Type[] outcomes() {
+        return outcomes;
+    }
+
+    /**
+     * @param outcomes
+     * 		The supported outcomes for the link created {@link Source}.
+     *
+     * @return this {@link SourceOptions} instance.
+     */
+    public SourceOptions outcomes(DeliveryState.Type... outcomes) {
+        this.outcomes = outcomes != null ? Arrays.copyOf(outcomes, outcomes.length) : null;
+        return self();
+    }
+
+    @Override
+    SourceOptions self() {
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SslOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SslOptions.java
new file mode 100644
index 0000000..4391d1c
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/SslOptions.java
@@ -0,0 +1,416 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+/**
+ * Options for configuration of the client SSL layer
+ */
+public class SslOptions implements Cloneable {
+
+    public static final String DEFAULT_STORE_TYPE = "jks";
+    public static final String DEFAULT_CONTEXT_PROTOCOL = "TLS";
+    public static final boolean DEFAULT_TRUST_ALL = false;
+    public static final boolean DEFAULT_VERIFY_HOST = true;
+    public static final List<String> DEFAULT_DISABLED_PROTOCOLS = Collections.unmodifiableList(Arrays.asList(new String[]{"SSLv2Hello", "SSLv3"}));
+    public static final int DEFAULT_SSL_PORT = 5671;
+    public static final boolean DEFAULT_ALLOW_NATIVE_SSL = false;
+
+    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
+    private static final String JAVAX_NET_SSL_KEY_STORE_TYPE = "javax.net.ssl.keyStoreType";
+    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
+    private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword";
+
+    private String keyStoreLocation;
+    private String keyStorePassword;
+    private String trustStoreLocation;
+    private String trustStorePassword;
+    private String keyStoreType;
+    private String trustStoreType;
+    private String[] enabledCipherSuites;
+    private String[] disabledCipherSuites;
+    private String[] enabledProtocols;
+    private String[] disabledProtocols = DEFAULT_DISABLED_PROTOCOLS.toArray(new String[0]);
+    private String contextProtocol = DEFAULT_CONTEXT_PROTOCOL;
+
+    private boolean trustAll = DEFAULT_TRUST_ALL;
+    private boolean verifyHost = DEFAULT_VERIFY_HOST;
+    private String keyAlias;
+    private int defaultSslPort = DEFAULT_SSL_PORT;
+    private SSLContext sslContextOverride;
+    private boolean sslEnabled;
+    private boolean allowNativeSSL = DEFAULT_ALLOW_NATIVE_SSL;
+
+    public SslOptions() {
+        keyStoreLocation(System.getProperty(JAVAX_NET_SSL_KEY_STORE));
+        keyStoreType(System.getProperty(JAVAX_NET_SSL_KEY_STORE_TYPE, DEFAULT_STORE_TYPE));
+        keyStorePassword(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD));
+        trustStoreLocation(System.getProperty(JAVAX_NET_SSL_TRUST_STORE));
+        trustStoreType(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_TYPE, DEFAULT_STORE_TYPE));
+        trustStorePassword(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD));
+    }
+
+    @Override
+    public SslOptions clone() {
+        return copyInto(new SslOptions());
+    }
+
+    public boolean sslEnabled() {
+        return sslEnabled;
+    }
+
+    public SslOptions sslEnabled(boolean enable) {
+        this.sslEnabled = enable;
+        return this;
+    }
+
+    /**
+     * @return the keyStoreLocation currently configured.
+     */
+    public String keyStoreLocation() {
+        return keyStoreLocation;
+    }
+
+    /**
+     * Sets the location on disk of the key store to use.
+     *
+     * @param keyStoreLocation
+     *        the keyStoreLocation to use to create the key manager.
+     *
+     * @return this options instance.
+     */
+    public SslOptions keyStoreLocation(String keyStoreLocation) {
+        this.keyStoreLocation = keyStoreLocation;
+        return this;
+    }
+
+    /**
+     * @return the keyStorePassword
+     */
+    public String keyStorePassword() {
+        return keyStorePassword;
+    }
+
+    /**
+     * @param keyStorePassword the keyStorePassword to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions keyStorePassword(String keyStorePassword) {
+        this.keyStorePassword = keyStorePassword;
+        return this;
+    }
+
+    /**
+     * @return the trustStoreLocation
+     */
+    public String trustStoreLocation() {
+        return trustStoreLocation;
+    }
+
+    /**
+     * @param trustStoreLocation the trustStoreLocation to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions trustStoreLocation(String trustStoreLocation) {
+        this.trustStoreLocation = trustStoreLocation;
+        return this;
+    }
+
+    /**
+     * @return the trustStorePassword
+     */
+    public String trustStorePassword() {
+        return trustStorePassword;
+    }
+
+    /**
+     * @param trustStorePassword the trustStorePassword to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions trustStorePassword(String trustStorePassword) {
+        this.trustStorePassword = trustStorePassword;
+        return this;
+    }
+
+    /**
+     * @param storeType
+     *        the format that the store files are encoded in.
+     *
+     * @return this options instance.
+     */
+    public SslOptions storeType(String storeType) {
+        keyStoreType(storeType);
+        trustStoreType(storeType);
+        return this;
+    }
+
+    /**
+     * @return the keyStoreType
+     */
+    public String keyStoreType() {
+        return keyStoreType;
+    }
+
+    /**
+     * @param keyStoreType
+     *        the format that the keyStore file is encoded in
+     *
+     * @return this options instance.
+     */
+    public SslOptions keyStoreType(String keyStoreType) {
+        this.keyStoreType = keyStoreType;
+        return this;
+    }
+
+    /**
+     * @return the trustStoreType
+     */
+    public String trustStoreType() {
+        return trustStoreType;
+    }
+
+    /**
+     * @param trustStoreType
+     *        the format that the trustStore file is encoded in
+     *
+     * @return this options instance.
+     */
+    public SslOptions trustStoreType(String trustStoreType) {
+        this.trustStoreType = trustStoreType;
+        return this;
+    }
+
+    /**
+     * @return the enabledCipherSuites
+     */
+    public String[] enabledCipherSuites() {
+        return enabledCipherSuites;
+    }
+
+    /**
+     * @param enabledCipherSuites the enabledCipherSuites to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions enabledCipherSuites(String... enabledCipherSuites) {
+        this.enabledCipherSuites = enabledCipherSuites;
+        return this;
+    }
+
+    /**
+     * @return the disabledCipherSuites
+     */
+    public String[] disabledCipherSuites() {
+        return disabledCipherSuites;
+    }
+
+    /**
+     * @param disabledCipherSuites the disabledCipherSuites to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions disabledCipherSuites(String... disabledCipherSuites) {
+        this.disabledCipherSuites = disabledCipherSuites;
+        return this;
+    }
+
+    /**
+     * @return the enabledProtocols or null if the defaults should be used
+     */
+    public String[] enabledProtocols() {
+        return enabledProtocols;
+    }
+
+    /**
+     * The protocols to be set as enabled.
+     *
+     * @param enabledProtocols the enabled protocols to set, or null if the defaults should be used.
+     *
+     * @return this options instance.
+     */
+    public SslOptions enabledProtocols(String... enabledProtocols) {
+        this.enabledProtocols = enabledProtocols;
+        return this;
+    }
+
+    /**
+     * @return the protocols to disable or null if none should be
+     */
+    public String[] disabledProtocols() {
+        return disabledProtocols;
+    }
+
+    /**
+     * The protocols to be disable.
+     *
+     * @param disabledProtocols the protocols to disable, or null if none should be.
+     *
+     * @return this options instance.
+     */
+    public SslOptions disabledProtocols(String... disabledProtocols) {
+        this.disabledProtocols = disabledProtocols;
+        return this;
+    }
+
+    /**
+    * @return the context protocol to use
+    */
+    public String contextProtocol() {
+        return contextProtocol;
+    }
+
+    /**
+     * The protocol value to use when creating an SSLContext via
+     * SSLContext.getInstance(protocol).
+     *
+     * @param contextProtocol the context protocol to use.
+     *
+     * @return this options instance.
+     */
+    public SslOptions contextProtocol(String contextProtocol) {
+        this.contextProtocol = contextProtocol;
+        return this;
+    }
+
+    /**
+     * @return the trustAll
+     */
+    public boolean trustAll() {
+        return trustAll;
+    }
+
+    /**
+     * @param trustAll the trustAll to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions trustAll(boolean trustAll) {
+        this.trustAll = trustAll;
+        return this;
+    }
+
+    /**
+     * @return the verifyHost
+     */
+    public boolean verifyHost() {
+        return verifyHost;
+    }
+
+    /**
+     * @param verifyHost the verifyHost to set
+     *
+     * @return this options instance.
+     */
+    public SslOptions verifyHost(boolean verifyHost) {
+        this.verifyHost = verifyHost;
+        return this;
+    }
+
+    /**
+     * @return the key alias
+     */
+    public String keyAlias() {
+        return keyAlias;
+    }
+
+    /**
+     * @param keyAlias the key alias to use
+     *
+     * @return this options instance.
+     */
+    public SslOptions keyAlias(String keyAlias) {
+        this.keyAlias = keyAlias;
+        return this;
+    }
+
+    public int defaultSslPort() {
+        return defaultSslPort;
+    }
+
+    public SslOptions defaultSslPort(int defaultSslPort) {
+        this.defaultSslPort = defaultSslPort;
+        return this;
+    }
+
+    public SslOptions sslContextOverride(SSLContext sslContextOverride) {
+        this.sslContextOverride = sslContextOverride;
+        return this;
+    }
+
+    public SSLContext sslContextOverride() {
+        return sslContextOverride;
+    }
+
+    /**
+     * @return true if the an native SSL based encryption layer is allowed to be used instead of the JDK.
+     */
+    public boolean allowNativeSSL() {
+        return allowNativeSSL;
+    }
+
+    /**
+     * @param allowNativeSSL
+     * 		Configure if the transport should attempt to use native SSL support if available.
+     *
+     * @return this options object.
+     */
+    public SslOptions allowNativeSSL(boolean allowNativeSSL) {
+        this.allowNativeSSL = allowNativeSSL;
+        return this;
+    }
+
+    /**
+     * Copy all configuration into the given {@link SslOptions} from this instance.
+     *
+     * @param other
+     * 		another {@link SslOptions} instance that will receive the configuration from this instance.
+     *
+     * @return the options instance that was copied into.
+     */
+    public SslOptions copyInto(SslOptions other) {
+        other.sslEnabled(sslEnabled());
+        other.keyStoreLocation(keyStoreLocation());
+        other.keyStorePassword(keyStorePassword());
+        other.trustStoreLocation(trustStoreLocation());
+        other.trustStorePassword(trustStorePassword());
+        other.keyStoreType(keyStoreType());
+        other.trustStoreType(trustStoreType());
+        other.enabledCipherSuites(enabledCipherSuites());
+        other.disabledCipherSuites(disabledCipherSuites());
+        other.enabledProtocols(enabledProtocols());
+        other.disabledProtocols(disabledProtocols());
+        other.trustAll(trustAll());
+        other.verifyHost(verifyHost());
+        other.keyAlias(keyAlias());
+        other.contextProtocol(contextProtocol());
+        other.defaultSslPort(defaultSslPort());
+        other.sslContextOverride(sslContextOverride());
+        other.allowNativeSSL(allowNativeSSL());
+
+        return other;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamDelivery.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamDelivery.java
new file mode 100644
index 0000000..4d1b296
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamDelivery.java
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * A specialized {@link Delivery} type that is returned from the {@link StreamReceiver}
+ * which can be used to read incoming large messages that are streamed via multiple incoming
+ * AMQP {@link Transfer} frames.
+ */
+public interface StreamDelivery extends Delivery {
+
+    /**
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamReceiver receiver();
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return a {@link StreamReceiverMessage} instance that can be used to read the incoming message stream.
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    StreamReceiverMessage message() throws ClientException;
+
+    /**
+     * Check if the {@link StreamDelivery} has been marked as aborted by the remote sender.
+     *
+     * @return true if this context has been marked as aborted previously.
+     */
+    boolean aborted();
+
+    /**
+     * Check if the {@link StreamDelivery} has been marked as complete by the remote sender.
+     *
+     * @return true if this context has been marked as being the complete.
+     */
+    boolean completed();
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamDelivery accept() throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamDelivery release() throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamDelivery reject(String condition, String description) throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamDelivery modified(boolean deliveryFailed, boolean undeliverableHere) throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamDelivery disposition(DeliveryState state, boolean settle) throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamReceiver} that originated this {@link StreamDelivery}.
+     */
+    @Override
+    StreamDelivery settle() throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiver.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiver.java
new file mode 100644
index 0000000..86e1b73
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiver.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * A receiver of large message content that is delivered in multiple {@link Transfer} frames from
+ * the remote.
+ */
+public interface StreamReceiver extends Receiver {
+
+    /**
+     * Blocking receive method that waits forever for the remote to provide a {@link StreamReceiverMessage} for consumption.
+     * <p>
+     * Receive calls will only grant credit on their own if a credit window is configured in the
+     * {@link StreamReceiverOptions} which is done by default.  If the client application has configured
+     * no credit window than this method will not grant any credit when it enters the wait for new
+     * incoming messages.
+     *
+     * @return a new {@link Delivery} received from the remote.
+     *
+     * @throws ClientException if the {@link StreamReceiver} or its parent is closed when the call to receive is made.
+     */
+    @Override
+    StreamDelivery receive() throws ClientException;
+
+    /**
+     * Blocking receive method that waits the given time interval for the remote to provide a
+     * {@link StreamReceiverMessage} for consumption.  The amount of time this method blocks is based on the
+     * timeout value. If timeout is equal to <code>-1</code> then it blocks until a Delivery is
+     * received. If timeout is equal to zero then it will not block and simply return a
+     * {@link StreamReceiverMessage} if one is available locally.  If timeout value is greater than zero then it
+     * blocks up to timeout amount of time.
+     * <p>
+     * Receive calls will only grant credit on their own if a credit window is configured in the
+     * {@link StreamReceiverOptions} which is done by default.  If the client application has not configured
+     * a credit window or granted credit manually this method will not automatically grant any credit
+     * when it enters the wait for a new incoming {@link StreamReceiverMessage}.
+     *
+     * @param timeout
+     *      The timeout value used to control how long the receive method waits for a new {@link Delivery}.
+     * @param unit
+     *      The unit of time that the given timeout represents.
+     *
+     * @return a new {@link StreamReceiverMessage} received from the remote.
+     *
+     * @throws ClientException if the {@link StreamReceiver} or its parent is closed when the call to receive is made.
+     */
+    @Override
+    StreamDelivery receive(long timeout, TimeUnit unit) throws ClientException;
+
+    /**
+     * Non-blocking receive method that either returns a message is one is immediately available or
+     * returns null if none is currently at hand.
+     *
+     * @return a new {@link StreamReceiverMessage} received from the remote or null if no pending deliveries are available.
+     *
+     * @throws ClientException if the {@link StreamReceiver} or its parent is closed when the call to try to receive is made.
+     */
+    @Override
+    StreamDelivery tryReceive() throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param credits
+     *      credit The number of credits to add to the {@link StreamReceiver} link.
+     *
+     * @return this {@link StreamReceiver} instance.
+     *
+     * @throws ClientException if an error occurs while attempting to add new {@link StreamReceiver} link credit.
+     */
+    @Override
+    StreamReceiver addCredit(int credits) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiverMessage.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiverMessage.java
new file mode 100644
index 0000000..b1f27e1
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiverMessage.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * A specialized {@link Message} type that represents a streamed delivery possibly
+ * spanning many incoming {@link Transfer} frames from the remote peer.  It is possible
+ * for various calls in this {@link StreamReceiverMessage} to block while awaiting the
+ * receipt of sufficient bytes to provide the result.
+ */
+public interface StreamReceiverMessage extends AdvancedMessage<InputStream> {
+
+    /**
+     * @return the {@link StreamDelivery} that is associated with the life-cycle of this {@link StreamReceiverMessage}
+     */
+    StreamDelivery delivery();
+
+    /**
+     * @return the {@link StreamReceiver} that this context was create under.
+     */
+    StreamReceiver receiver();
+
+    /**
+     * Check if the {@link StreamDelivery} that was assigned to this {@link StreamReceiverMessage} has been
+     * marked as aborted by the remote.
+     *
+     * @return true if this context has been marked as aborted previously.
+     */
+    boolean aborted();
+
+    /**
+     * Check if the {@link StreamDelivery} that was assigned to this {@link StreamReceiverMessage} has been
+     * marked as complete by the remote.
+     *
+     * @return true if this context has been marked as being the complete.
+     */
+    boolean completed();
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiverOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiverOptions.java
new file mode 100644
index 0000000..684dcfd
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamReceiverOptions.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+
+/**
+ * Options class that controls various aspects of a {@link StreamReceiver} instance and how
+ * a streamed message transfer is written.
+ */
+public class StreamReceiverOptions extends ReceiverOptions {
+
+    /**
+     * Defines the default read buffering size which is used to control how much incoming
+     * data can be buffered before the remote has back pressured applied to avoid out of
+     * memory conditions.
+     */
+    public static final int DEFAULT_READ_BUFFER_SIZE = SessionOptions.DEFAULT_SESSION_INCOMING_CAPACITY;
+
+    private int readBufferSize = DEFAULT_READ_BUFFER_SIZE;
+
+    /**
+     * Creates a {@link StreamReceiverOptions} instance with default values for all options
+     */
+    public StreamReceiverOptions() {
+    }
+
+    /**
+     * Create a {@link StreamReceiverOptions} instance that copies all configuration from the given
+     * {@link StreamReceiverOptions} instance.
+     *
+     * @param options
+     *      The options instance to copy all configuration values from.
+     */
+    public StreamReceiverOptions(StreamReceiverOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    @Override
+    public StreamReceiverOptions clone() {
+        return copyInto(new StreamReceiverOptions());
+    }
+
+    /**
+     * Copy all options from this {@link StreamReceiverOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this {@link StreamReceiverOptions} class for chaining.
+     */
+    protected StreamReceiverOptions copyInto(StreamReceiverOptions other) {
+        super.copyInto(other);
+
+        other.readBufferSize(readBufferSize);
+
+        return this;
+    }
+
+    /**
+     * @return the configured session capacity for the parent session of the {@link StreamReceiver}.
+     */
+    public int readBufferSize() {
+        return readBufferSize;
+    }
+
+    /**
+     * Sets the incoming buffer capacity (in bytes) that the {@link StreamReceiver}.
+     * <p>
+     * When the remote peer is sending incoming data for a {@link StreamReceiverMessage} the amount that is stored
+     * in memory before back pressure is applied to the remote is controlled by this option.  If the user
+     * does not read incoming data as it arrives this limit can prevent out of memory errors that might
+     * otherwise arise as the remote attempts to immediately send all contents of very large message payloads.
+     *
+     * @param readBufferSize
+     *       The number of bytes that the {@link StreamReceiver} will buffer for a given {@link StreamReceiverMessage}.
+     *
+     * @return this {@link StreamReceiverOptions} instance.
+     */
+    public StreamReceiverOptions readBufferSize(int readBufferSize) {
+        this.readBufferSize = readBufferSize;
+        return this;
+    }
+
+    //----- Override super methods to customize the return type
+
+    @Override
+    public StreamReceiverOptions autoAccept(boolean autoAccept) {
+        return (StreamReceiverOptions) super.autoAccept(autoAccept);
+    }
+
+    @Override
+    public StreamReceiverOptions autoSettle(boolean autoSettle) {
+        return (StreamReceiverOptions) super.autoSettle(autoSettle);
+    }
+
+    @Override
+    public StreamReceiverOptions deliveryMode(DeliveryMode deliveryMode) {
+        return (StreamReceiverOptions) super.deliveryMode(deliveryMode);
+    }
+
+    @Override
+    public StreamReceiverOptions linkName(String linkName) {
+        return (StreamReceiverOptions) super.linkName(linkName);
+    }
+
+    @Override
+    public StreamReceiverOptions creditWindow(int creditWindow) {
+        return (StreamReceiverOptions) super.creditWindow(creditWindow);
+    }
+
+    @Override
+    public StreamReceiverOptions closeTimeout(long closeTimeout) {
+        return (StreamReceiverOptions) super.closeTimeout(closeTimeout);
+    }
+
+    @Override
+    public StreamReceiverOptions openTimeout(long openTimeout) {
+        return (StreamReceiverOptions) super.openTimeout(openTimeout);
+    }
+
+    @Override
+    public StreamReceiverOptions drainTimeout(long drainTimeout) {
+        return (StreamReceiverOptions) super.drainTimeout(drainTimeout);
+    }
+
+    @Override
+    public StreamReceiverOptions requestTimeout(long requestTimeout) {
+        return (StreamReceiverOptions) super.requestTimeout(requestTimeout);
+    }
+
+    @Override
+    public StreamReceiverOptions offeredCapabilities(String... offeredCapabilities) {
+        return (StreamReceiverOptions) super.offeredCapabilities(offeredCapabilities);
+    }
+
+    @Override
+    public StreamReceiverOptions desiredCapabilities(String... desiredCapabilities) {
+        return (StreamReceiverOptions) super.desiredCapabilities(desiredCapabilities);
+    }
+
+    @Override
+    public StreamReceiverOptions properties(Map<String, Object> properties) {
+        return (StreamReceiverOptions) super.properties(properties);
+    }
+
+    @Override
+    protected StreamReceiverOptions copyInto(ReceiverOptions other) {
+        return (StreamReceiverOptions) super.copyInto(other);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSender.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSender.java
new file mode 100644
index 0000000..0f7cb57
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSender.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * Sending link implementation that allows sending of large message payload data in
+ * multiple transfers to reduce memory overhead of large message sends.
+ */
+public interface StreamSender extends Sender {
+
+    /**
+     * Creates and returns a new {@link StreamSenderMessage} that can be used by the caller to perform
+     * streaming sends of large message payload data.
+     *
+     * @return a new {@link StreamSenderMessage} that can be used to stream message data to the remote.
+     *
+     * @throws ClientException if an error occurs while initiating a new streaming send message.
+     */
+    StreamSenderMessage beginMessage() throws ClientException;
+
+    /**
+     * Creates and returns a new {@link StreamSenderMessage} that can be used by the caller to perform
+     * streaming sends of large message payload data.
+     *
+     * @param deliveryAnnotations
+     *      the delivery annotations that should be included in the sent {@link StreamSenderMessage}.
+     *
+     * @return a new {@link StreamSenderMessage} that can be used to stream message data to the remote.
+     *
+     * @throws ClientException if an error occurs while initiating a new streaming send message.
+     */
+    StreamSenderMessage beginMessage(Map<String, Object> deliveryAnnotations) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSenderMessage.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSenderMessage.java
new file mode 100644
index 0000000..c0d7fc1
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSenderMessage.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.io.OutputStream;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Streaming Message Tracker object used to operate on and track the state of a streamed message
+ * at the remote. The {@link StreamSenderMessage} allows for local settlement and disposition management
+ * as well as waiting for remote settlement of a streamed message.
+ */
+public interface StreamSenderMessage extends AdvancedMessage<OutputStream> {
+
+    /**
+     * @return The {@link Tracker} assigned to monitor the life-cycle of this {@link StreamSenderMessage}
+     */
+    StreamTracker tracker();
+
+    /**
+     * @return the {@link Sender} that was used to send the delivery that is being tracked.
+     */
+    StreamSender sender();
+
+    /**
+     * Sets the configured message format value that will be set on the first outgoing
+     * AMQP {@link Transfer} frame for the delivery that comprises this streamed message.
+     * This value can only be updated before write operation is attempted and will throw
+     * an {@link ClientIllegalStateException} if any attempt to alter the value is made
+     * following a write.
+     *
+     * @param messageFormat
+     *      The assigned AMQP message format for this streamed message.
+     *
+     * @return this {@link StreamSenderMessage} instance.
+     *
+     * @throws ClientException if an error occurs while attempting set the message format.
+     */
+    @Override
+    StreamSenderMessage messageFormat(int messageFormat) throws ClientException;
+
+    /**
+     * Marks the currently streaming message as being complete.
+     * <p>
+     * Marking a message as complete finalizes the {@link SendContext} and causes a
+     * final {@link Transfer} frame to be sent to the remote indicating that the ongoing
+     * streaming delivery is done and no more message data will arrive.
+     *
+     * @return this {@link StreamSenderMessage} instance.
+     *
+     * @throws ClientException if an error occurs while initiating the completion operation.
+     */
+    StreamSenderMessage complete() throws ClientException;
+
+    /**
+     * @return true if this message has been marked as being complete.
+     */
+    boolean completed();
+
+    /**
+     * Marks the currently streaming message as being aborted. Once aborted no further
+     * writes regardless of whether any writes have yet been performed or not.
+     *
+     * @param aborted
+     *      Should the message be marked as having been aborted.
+     *
+     * @return this {@link StreamSenderMessage} instance.
+     *
+     * @throws ClientException if an error occurs while initiating the abort operation.
+     */
+    StreamSenderMessage abort() throws ClientException;
+
+    /**
+     * @return true if this {@link SendContext} has been marked as aborted previously.
+     */
+    boolean aborted();
+
+    /**
+     * Creates an {@link OutputStream} instance configured with the given options which will
+     * write the bytes as the payload of one or more AMQP {@link Data} sections based on the
+     * provided configuration..
+     * <p>
+     * The returned {@link OutputStream} can be used to write the payload of an AMQP Message in
+     * chunks when the source is not readily available in memory or as part of a larger streams
+     * based component.  The {@link Data} section based stream allows for control over the AMQP
+     * message {@link Section} values that are sent but does the encoding itself.  For stream
+     * of message data where the content source already consists of an AMQP encoded message refer
+     * to the {@link #rawOutputStream(OutputStreamOptions)} method.
+     *
+     * @param options
+     *      The stream options to use to configure the returned {@link MessageOutputStream}
+     *
+     * @return a {@link OutputStream} instance configured using the given options.
+     *
+     * @throws ClientException if an error occurs while creating the {@link OutputStream}.
+     *
+     * @see #rawOutputStream(OutputStreamOptions)
+     */
+    OutputStream body(OutputStreamOptions options) throws ClientException;
+
+    /**
+     * Creates an {@link OutputStream} instance that writes the bytes given without additional
+     * encoding or transformation.  Using this stream option disables use of any other {@link Message}
+     * APIs and the message transfer is completed upon close of this {@link OutputStream}.
+     * <p>
+     * The returned {@link OutputStream} can be used to write the payload of an AMQP Message in
+     * chunks when the source is not readily available in memory or as part of a larger streams
+     * based component.  The source of the bytes written to the {@link OutputStream} should
+     * consist of already encoded AMQP {@link Message} data.  For an {@link OutputStream} that
+     * performs the encoding of message data refer to the {@link #body(OutputStreamOptions)}
+     * method.
+     *
+     * @return an {@link OutputStream} instance that performs no encoding.
+     *
+     * @throws ClientException if an error occurs while creating the {@link OutputStream}.
+     *
+     * @see #dataOutputStream(OutputStreamOptions)
+     */
+    OutputStream rawOutputStream() throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSenderOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSenderOptions.java
new file mode 100644
index 0000000..cdb30f4
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamSenderOptions.java
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+
+/**
+ * Options class that controls various aspects of a {@link StreamSenderMessage} instance and how
+ * a streamed message transfer is written.
+ */
+public class StreamSenderOptions extends SenderOptions {
+
+    /**
+     * Defines the default pending write buffering size which is used to control how much outgoing
+     * data can be buffered for local writing before the sender has back pressured applied to avoid
+     * out of memory conditions due to overly large pending batched writes.
+     */
+    public static final int DEFAULT_PENDING_WRITES_BUFFER_SIZE = SessionOptions.DEFAULT_SESSION_OUTGOING_CAPACITY;
+
+    private int pendingWritesBufferSize = DEFAULT_PENDING_WRITES_BUFFER_SIZE;
+
+    /**
+     * Defines the default minimum size that the context write buffer will allocate
+     * which drives the interval auto flushing of written data for this context.
+     */
+    public static final int MIN_BUFFER_SIZE_LIMIT = 256;
+
+    private int writeBufferSize;
+
+    /**
+     * Creates a {@link StreamSenderOptions} instance with default values for all options
+     */
+    public StreamSenderOptions() {
+    }
+
+    @Override
+    public StreamSenderOptions clone() {
+        return copyInto(new StreamSenderOptions());
+    }
+
+    /**
+     * Create a {@link StreamSenderOptions} instance that copies all configuration from the given
+     * {@link StreamSenderOptions} instance.
+     *
+     * @param options
+     *      The options instance to copy all configuration values from.
+     */
+    public StreamSenderOptions(StreamSenderOptions options) {
+        if (options != null) {
+            options.copyInto(this);
+        }
+    }
+
+    /**
+     * Copy all options from this {@link StreamSenderOptions} instance into the instance
+     * provided.
+     *
+     * @param other
+     *      the target of this copy operation.
+     *
+     * @return this {@link StreamSenderOptions} class for chaining.
+     */
+    protected StreamSenderOptions copyInto(StreamSenderOptions other) {
+        super.copyInto(other);
+
+        other.writeBufferSize(writeBufferSize);
+
+        return this;
+    }
+
+    /**
+     * @return the configured context write buffering limit for the associated {@link StreamSender}
+     */
+    public int writeBufferSize() {
+        return writeBufferSize;
+    }
+
+    /**
+     * Sets the overall number of bytes the stream sender will buffer before automatically flushing the
+     * currently buffered bytes.  By default the stream sender implementation chooses a value for this
+     * buffer limit based on the configured frame size limits of the connection.
+     *
+     * @param writeBufferSize
+     *       The number of bytes that can be written before the context performs a flush operation.
+     *
+     * @return this {@link StreamSenderOptions} instance.
+     */
+    public StreamSenderOptions writeBufferSize(int writeBufferSize) {
+        this.writeBufferSize = writeBufferSize;
+        return this;
+    }
+
+    /**
+     * @return the configured pending write buffering limit for the associated {@link StreamSender}
+     */
+    public int pendingWritesBufferSize() {
+        return this.pendingWritesBufferSize;
+    }
+
+    /**
+     * Sets the overall number of bytes the stream sender will allow to be pending for write before applying
+     * back pressure to the stream write caller.  By default the stream sender implementation chooses a value
+     * for this pending write limit based on the configured frame size limits of the connection.  This is an
+     * advanced option and should not be used unless the impact of doing so is understood by the user.
+     *
+     * @param pendingWritesBufferSize
+     *       The number of bytes that can be pending for write before the sender applies back pressure.
+     *
+     * @return this {@link StreamSenderOptions} instance.
+     */
+    public StreamSenderOptions pendingWritesBufferSize(int pendingWritesBufferSize) {
+        this.pendingWritesBufferSize = pendingWritesBufferSize;
+        return this;
+    }
+
+    //----- Override super methods to return this options type for ease of use
+
+    @Override
+    public StreamSenderOptions linkName(String linkName) {
+        return (StreamSenderOptions) super.linkName(linkName);
+    }
+
+    @Override
+    public StreamSenderOptions autoSettle(boolean autoSettle) {
+        return (StreamSenderOptions) super.autoSettle(autoSettle);
+    }
+
+    @Override
+    public StreamSenderOptions deliveryMode(DeliveryMode deliveryMode) {
+        return (StreamSenderOptions) super.deliveryMode(deliveryMode);
+    }
+
+    @Override
+    public StreamSenderOptions closeTimeout(long closeTimeout) {
+        return (StreamSenderOptions) super.closeTimeout(closeTimeout);
+    }
+
+    @Override
+    public StreamSenderOptions openTimeout(long openTimeout) {
+        return (StreamSenderOptions) super.openTimeout(openTimeout);
+    }
+
+    @Override
+    public StreamSenderOptions sendTimeout(long sendTimeout) {
+        return (StreamSenderOptions) super.sendTimeout(sendTimeout);
+    }
+
+    @Override
+    public StreamSenderOptions requestTimeout(long requestTimeout) {
+        return (StreamSenderOptions) super.requestTimeout(requestTimeout);
+    }
+
+    @Override
+    public StreamSenderOptions offeredCapabilities(String... offeredCapabilities) {
+        return (StreamSenderOptions) super.offeredCapabilities(offeredCapabilities);
+    }
+
+    @Override
+    public StreamSenderOptions desiredCapabilities(String... desiredCapabilities) {
+        return (StreamSenderOptions) super.desiredCapabilities(desiredCapabilities);
+    }
+
+    @Override
+    public StreamSenderOptions properties(Map<String, Object> properties) {
+        return (StreamSenderOptions) super.properties(properties);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamTracker.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamTracker.java
new file mode 100644
index 0000000..362758f
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/StreamTracker.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * Special StreamSender related {@link Tracker} that is linked to any {@link StreamSenderMessage}
+ * instance and provides the {@link Tracker} functions for those types of messages.
+ */
+public interface StreamTracker extends Tracker {
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return the {@link StreamSender} that is associated with this {@link StreamTracker}.
+     */
+    @Override
+    StreamSender sender();
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return this {@link StreamTracker} instance.
+     */
+    @Override
+    StreamTracker settle() throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    Future<Tracker> settlementFuture();
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return this {@link StreamTracker} instance.
+     */
+    @Override
+    StreamTracker disposition(DeliveryState state, boolean settle) throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return this {@link StreamTracker} instance.
+     */
+    @Override
+    StreamTracker awaitSettlement() throws ClientException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return this {@link StreamTracker} instance.
+     */
+    @Override
+    StreamTracker awaitSettlement(long timeout, TimeUnit unit) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Target.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Target.java
new file mode 100644
index 0000000..56357b6
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Target.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The Target of messages.
+ *
+ * For an opened {@link Sender} or {@link Receiver} the Target properties exposes the
+ * remote {@link Target} configuration.
+ */
+public interface Target {
+
+    /**
+     * @return the address of the Source node.
+     */
+    String address();
+
+    /**
+     * @return the durabilityMode of this Source node.
+     */
+    DurabilityMode durabilityMode();
+
+    /**
+     * @return the timeout assigned to this Source node in seconds.
+     */
+    long timeout();
+
+    /**
+     * @return the {@link ExpiryPolicy} of this Source node.
+     */
+    ExpiryPolicy expiryPolicy();
+
+    /**
+     * @return true if the Source node dynamically on-demand
+     */
+    boolean dynamic();
+
+    /**
+     * @return the properties of the dynamically created Source node.
+     */
+    Map<String, Object> dynamicNodeProperties();
+
+    /**
+     * @return the set of capabilities available on this Source node.
+     */
+    Set<String> capabilities();
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TargetOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TargetOptions.java
new file mode 100644
index 0000000..f5c6b55
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TargetOptions.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+/**
+ * Options type that carries configuration for link Target types.
+ */
+public final class TargetOptions extends TerminusOptions<TargetOptions> {
+
+    public TargetOptions copyInto(TargetOptions other) {
+        super.copyInto(other);
+        return this;
+    }
+
+    @Override
+    public TargetOptions clone() {
+        return copyInto(new TargetOptions());
+    }
+
+    @Override
+    TargetOptions self() {
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TerminusOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TerminusOptions.java
new file mode 100644
index 0000000..f0b5bb1
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TerminusOptions.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+
+/**
+ * Base options type for configuration of {@link Source} and {@link Target} types
+ * used by {@link Sender} and {@link Receiver} end points.
+ *
+ * @param <E> the subclass that implements this terminus options type.
+ */
+public abstract class TerminusOptions<E extends TerminusOptions<E>> {
+
+    private DurabilityMode durabilityMode;
+    private long timeout = -1;
+    private ExpiryPolicy expiryPolicy;
+    private String[] capabilities;
+
+    abstract E self();
+
+    /**
+     * @return the durabilityMode
+     */
+    public DurabilityMode durabilityMode() {
+        return durabilityMode;
+    }
+
+    /**
+     * @param durabilityMode the durabilityMode to set
+     *
+     * @return this options instance.
+     */
+    public E durabilityMode(DurabilityMode durabilityMode) {
+        this.durabilityMode = durabilityMode;
+        return self();
+    }
+
+    /**
+     * @return the timeout
+     */
+    public long timeout() {
+        return timeout;
+    }
+
+    /**
+     * @param timeout the timeout to set
+     *
+     * @return this options instance.
+     */
+    public E timeout(long timeout) {
+        if (timeout < 0 || timeout > UnsignedInteger.MAX_VALUE.longValue()) {
+            throw new IllegalArgumentException("Timeout value must be in the range of an unsigned intenger");
+        }
+        this.timeout = timeout;
+        return self();
+    }
+
+    /**
+     * @return the expiryPolicy
+     */
+    public ExpiryPolicy expiryPolicy() {
+        return expiryPolicy;
+    }
+
+    /**
+     * @param expiryPolicy the expiryPolicy to set
+     *
+     * @return this options instance.
+     */
+    public E expiryPolicy(ExpiryPolicy expiryPolicy) {
+        this.expiryPolicy = expiryPolicy;
+        return self();
+    }
+
+    /**
+     * @return the capabilities
+     */
+    public String[] capabilities() {
+        return capabilities;
+    }
+
+    /**
+     * @param capabilities the capabilities to set
+     *
+     * @return this options instance.
+     */
+    public E capabilities(String... capabilities) {
+        this.capabilities = capabilities;
+        return self();
+    }
+
+    protected void copyInto(TerminusOptions<E> other) {
+        other.durabilityMode(durabilityMode);
+        other.expiryPolicy(expiryPolicy);
+        if (timeout > 0) {
+            other.timeout(timeout);
+        }
+        if (capabilities != null) {
+            other.capabilities(Arrays.copyOf(capabilities, capabilities.length));
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Tracker.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Tracker.java
new file mode 100644
index 0000000..0af7748
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/Tracker.java
@@ -0,0 +1,151 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientDeliveryStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+
+/**
+ * Tracker object used to track the state of a sent {@link Message} at the remote
+ * and allows for local settlement and disposition management.
+ */
+public interface Tracker {
+
+    /**
+     * @return the {@link Sender} that was used to send the delivery that is being tracked.
+     */
+    Sender sender();
+
+    /**
+     * Settles the delivery locally, if not {@link SenderOptions#autoSettle() auto-settling}.
+     *
+     * @return this {@link Tracker} instance.
+     *
+     * @throws ClientException if an error occurs while performing the settlement.
+     */
+    Tracker settle() throws ClientException;
+
+    /**
+     * @return true if the sent message has been locally settled.
+     */
+    boolean settled();
+
+    /**
+     * Gets the current local state for the tracked delivery.
+     *
+     * @return the delivery state
+     */
+    DeliveryState state();
+
+    /**
+     * Gets the current remote state for the tracked delivery.
+     *
+     * @return the remote {@link DeliveryState} once a value is received from the remote.
+     */
+    DeliveryState remoteState();
+
+    /**
+     * Gets whether the delivery was settled by the remote peer yet.
+     *
+     * @return whether the delivery is remotely settled
+     */
+    boolean remoteSettled();
+
+    /**
+     * Updates the DeliveryState, and optionally settle the delivery as well.
+     *
+     * @param state
+     *            the delivery state to apply
+     * @param settle
+     *            whether to {@link #settle()} the delivery at the same time
+     *
+     * @return this {@link Tracker} instance.
+     *
+     * @throws ClientException if an error occurs while applying the given disposition
+     */
+    Tracker disposition(DeliveryState state, boolean settle) throws ClientException;
+
+    /**
+     * Returns a future that can be used to wait for the remote to acknowledge receipt of
+     * a sent message by settling it.
+     *
+     * @return a {@link Future} that can be used to wait on remote settlement.
+     */
+    Future<Tracker> settlementFuture();
+
+    /**
+     * Waits if necessary for the remote to settle the sent delivery unless it has
+     * either already been settled or the original delivery was sent settled in which
+     * case the remote will not send a {@link Disposition} back.
+     *
+     * @return this {@link Tracker} instance.
+     *
+     * @throws ClientException if an error occurs while awaiting the remote settlement.
+     */
+    Tracker awaitSettlement() throws ClientException;
+
+    /**
+     * Waits if necessary for the remote to settle the sent delivery unless it has
+     * either already been settled or the original delivery was sent settled in which
+     * case the remote will not send a {@link Disposition} back.
+     *
+     * @param timeout
+     *      the maximum time to wait for the remote to settle.
+     * @param unit
+     *      the time unit of the timeout argument.
+     *
+     * @return this {@link Tracker} instance.
+     *
+     * @throws ClientException if an error occurs while awaiting the remote settlement.
+     */
+    Tracker awaitSettlement(long timeout, TimeUnit unit) throws ClientException;
+
+    /**
+     * Waits if necessary for the remote to settle the sent delivery with an {@link Accepted}
+     * disposition unless it has either already been settled and accepted or the original delivery
+     * was sent settled in which case the remote will not send a {@link Disposition} back.
+     *
+     * @return this {@link Tracker} instance.
+     *
+     * @throws ClientDeliveryStateException if the remote sends a disposition other than Accepted.
+     * @throws ClientException if an error occurs while awaiting the remote settlement.
+     */
+    Tracker awaitAccepted() throws ClientException;
+
+    /**
+     * Waits if necessary for the remote to settle the sent delivery with an {@link Accepted}
+     * disposition unless it has either already been settled and accepted or the original delivery
+     * was sent settled in which case the remote will not send a {@link Disposition} back.
+     *
+     * @param timeout
+     *      the maximum time to wait for the remote to settle.
+     * @param unit
+     *      the time unit of the timeout argument.
+     *
+     * @return this {@link Tracker} instance.
+     *
+     * @throws ClientDeliveryStateException if the remote sends a disposition other than Accepted.
+     * @throws ClientException if an error occurs while awaiting the remote settlement.
+     */
+    Tracker awaitAccepted(long timeout, TimeUnit unit) throws ClientException;
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TransportOptions.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TransportOptions.java
new file mode 100644
index 0000000..e3f515b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/TransportOptions.java
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Encapsulates all the Transport options in one configuration object.
+ */
+public class TransportOptions implements Cloneable {
+
+    public static final int DEFAULT_SEND_BUFFER_SIZE = 64 * 1024;
+    public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_SEND_BUFFER_SIZE;
+    public static final int DEFAULT_TRAFFIC_CLASS = 0;
+    public static final boolean DEFAULT_TCP_NO_DELAY = true;
+    public static final boolean DEFAULT_TCP_KEEP_ALIVE = false;
+    public static final int DEFAULT_SO_LINGER = Integer.MIN_VALUE;
+    public static final int DEFAULT_SO_TIMEOUT = -1;
+    public static final int DEFAULT_CONNECT_TIMEOUT = 60000;
+    public static final int DEFAULT_TCP_PORT = 5672;
+    public static final boolean DEFAULT_ALLOW_NATIVE_IO = true;
+    public static final boolean DEFAULT_TRACE_BYTES = false;
+    public static final int DEFAULT_LOCAL_PORT = 0;
+    public static final boolean DEFAULT_USE_WEBSOCKETS = false;
+    public static final int DEFAULT_WEBSOCKET_MAX_FRAME_SIZE = 65535;
+    public static final String[] DEFAULT_NATIVEIO_PREFERENCES = { "EPOLL", "KQUEUE" };
+
+    private int sendBufferSize = DEFAULT_SEND_BUFFER_SIZE;
+    private int receiveBufferSize = DEFAULT_RECEIVE_BUFFER_SIZE;
+    private int trafficClass = DEFAULT_TRAFFIC_CLASS;
+    private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;
+    private int soTimeout = DEFAULT_SO_TIMEOUT;
+    private int soLinger = DEFAULT_SO_LINGER;
+    private boolean tcpKeepAlive = DEFAULT_TCP_KEEP_ALIVE;
+    private boolean tcpNoDelay = DEFAULT_TCP_NO_DELAY;
+    private int defaultTcpPort = DEFAULT_TCP_PORT;
+    private String localAddress;
+    private int localPort = DEFAULT_LOCAL_PORT;
+    private boolean allowNativeIO = DEFAULT_ALLOW_NATIVE_IO;
+    private String[] nativeIOPeference = DEFAULT_NATIVEIO_PREFERENCES;
+    private boolean traceBytes = DEFAULT_TRACE_BYTES;
+    private boolean useWebSockets = DEFAULT_USE_WEBSOCKETS;
+    private String webSocketPath;
+    private int webSocketMaxFrameSize = DEFAULT_WEBSOCKET_MAX_FRAME_SIZE;
+
+    private final Map<String, String> webSocketHeaders = new HashMap<>();
+
+    @Override
+    public TransportOptions clone() {
+        return copyInto(new TransportOptions());
+    }
+
+    /**
+     * @return the currently set send buffer size in bytes.
+     */
+    public int sendBufferSize() {
+        return sendBufferSize;
+    }
+
+    /**
+     * Sets the send buffer size in bytes, the value must be greater than zero
+     * or an {@link IllegalArgumentException} will be thrown.
+     *
+     * @param sendBufferSize
+     *        the new send buffer size for the TCP Transport.
+     *
+     * @return this options instance.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public TransportOptions sendBufferSize(int sendBufferSize) {
+        if (sendBufferSize <= 0) {
+            throw new IllegalArgumentException("The send buffer size must be > 0");
+        }
+
+        this.sendBufferSize = sendBufferSize;
+        return this;
+    }
+
+    /**
+     * @return the currently configured receive buffer size in bytes.
+     */
+    public int receiveBufferSize() {
+        return receiveBufferSize;
+    }
+
+    /**
+     * Sets the receive buffer size in bytes, the value must be greater than zero
+     * or an {@link IllegalArgumentException} will be thrown.
+     *
+     * @param receiveBufferSize
+     *        the new receive buffer size for the TCP Transport.
+     *
+     * @return this options instance.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public TransportOptions receiveBufferSize(int receiveBufferSize) {
+        if (receiveBufferSize <= 0) {
+            throw new IllegalArgumentException("The send buffer size must be > 0");
+        }
+
+        this.receiveBufferSize = receiveBufferSize;
+        return this;
+    }
+
+    /**
+     * @return the currently configured traffic class value.
+     */
+    public int trafficClass() {
+        return trafficClass;
+    }
+
+    /**
+     * Sets the traffic class value used by the TCP connection, valid
+     * range is between 0 and 255.
+     *
+     * @param trafficClass
+     *        the new traffic class value.
+     *
+     * @return this options instance.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public TransportOptions trafficClass(int trafficClass) {
+        if (trafficClass < 0 || trafficClass > 255) {
+            throw new IllegalArgumentException("Traffic class must be in the range [0..255]");
+        }
+
+        this.trafficClass = trafficClass;
+        return this;
+    }
+
+    public int soTimeout() {
+        return soTimeout;
+    }
+
+    public TransportOptions soTimeout(int soTimeout) {
+        this.soTimeout = soTimeout;
+        return this;
+    }
+
+    public boolean tcpNoDelay() {
+        return tcpNoDelay;
+    }
+
+    public TransportOptions tcpNoDelay(boolean tcpNoDelay) {
+        this.tcpNoDelay = tcpNoDelay;
+        return this;
+    }
+
+    public int soLinger() {
+        return soLinger;
+    }
+
+    public TransportOptions soLinger(int soLinger) {
+        this.soLinger = soLinger;
+        return this;
+    }
+
+    public boolean tcpKeepAlive() {
+        return tcpKeepAlive;
+    }
+
+    public TransportOptions tcpKeepAlive(boolean keepAlive) {
+        this.tcpKeepAlive = keepAlive;
+        return this;
+    }
+
+    public int connectTimeout() {
+        return connectTimeout;
+    }
+
+    public TransportOptions connectTimeout(int connectTimeout) {
+        this.connectTimeout = connectTimeout;
+        return this;
+    }
+
+    public int defaultTcpPort() {
+        return defaultTcpPort;
+    }
+
+    public TransportOptions defaultTcpPort(int defaultTcpPort) {
+        this.defaultTcpPort = defaultTcpPort;
+        return this;
+    }
+
+    public String localAddress() {
+        return localAddress;
+    }
+
+    public TransportOptions localAddress(String localAddress) {
+        this.localAddress = localAddress;
+        return this;
+    }
+
+    public int localPort() {
+        return localPort;
+    }
+
+    public TransportOptions localPort(int localPort) {
+        this.localPort = localPort;
+        return this;
+    }
+
+    /**
+     * @return true if an native IO library can be used if available on this platform instead of the JDK IO.
+     */
+    public boolean allowNativeIO() {
+        return allowNativeIO;
+    }
+
+    /**
+     * Determines if the a native IO implementation is preferred to the JDK based IO.
+     *
+     * @param allowNativeIO
+     * 		should use of available native transport be allowed if one is available.
+     *
+     * @return this options instance.
+     */
+    public TransportOptions allowNativeIO(boolean allowNativeIO) {
+        this.allowNativeIO = allowNativeIO;
+        return this;
+    }
+
+    /**
+     * @return the nativeIOPeference
+     */
+    public String[] nativeIOPeference() {
+        return nativeIOPeference;
+    }
+
+    /**
+     * @param nativeIOPeference the nativeIOPeference to set
+     */
+    public void nativeIOPeference(String... nativeIOPeference) {
+        if (nativeIOPeference == null || nativeIOPeference.length == 0 || nativeIOPeference.length == 1 && nativeIOPeference[0] == null) {
+            this.nativeIOPeference = DEFAULT_NATIVEIO_PREFERENCES;
+        } else {
+            this.nativeIOPeference = nativeIOPeference;
+        }
+    }
+
+    /**
+     * @return true if the transport should enable byte tracing
+     */
+    public boolean traceBytes() {
+        return traceBytes;
+    }
+
+    /**
+     * Determines if the transport should add a logger for bytes in / out
+     *
+     * @param traceBytes
+     * 		should the transport log the bytes in and out.
+     *
+     * @return this options instance.
+     */
+    public TransportOptions traceBytes(boolean traceBytes) {
+        this.traceBytes = traceBytes;
+        return this;
+    }
+
+    public boolean useWebSockets() {
+        return useWebSockets;
+    }
+
+    public TransportOptions useWebSockets(boolean webSockets) {
+        this.useWebSockets = webSockets;
+        return this;
+    }
+
+    public String webSocketPath() {
+        return webSocketPath;
+    }
+
+    public TransportOptions webSocketPath(String webSocketPath) {
+        this.webSocketPath = webSocketPath;
+        return this;
+    }
+
+    public Map<String, String> webSocketHeaders() {
+        return webSocketHeaders;
+    }
+
+    public TransportOptions addWebSocketHeader(String key, String value) {
+        this.webSocketHeaders.put(key, value);
+        return this;
+    }
+
+    public TransportOptions webSocketMaxFrameSize(int maxFrameSize) {
+        this.webSocketMaxFrameSize = maxFrameSize;
+        return this;
+    }
+
+    public int webSocketMaxFrameSize() {
+        return webSocketMaxFrameSize;
+    }
+
+    /**
+     * Copy all configuration into the given {@link TransportOptions} from this instance.
+     *
+     * @param other
+     * 		another {@link TransportOptions} instance that will receive the configuration from this instance.
+     *
+     * @return the options instance that was copied into.
+     */
+    public TransportOptions copyInto(TransportOptions other) {
+        other.connectTimeout(connectTimeout());
+        other.receiveBufferSize(receiveBufferSize());
+        other.sendBufferSize(sendBufferSize());
+        other.soLinger(soLinger());
+        other.soTimeout(soTimeout());
+        other.tcpKeepAlive(tcpKeepAlive());
+        other.tcpNoDelay(tcpNoDelay());
+        other.trafficClass(trafficClass());
+        other.defaultTcpPort(defaultTcpPort());
+        other.allowNativeIO(allowNativeIO());
+        other.nativeIOPeference(nativeIOPeference());
+        other.traceBytes(traceBytes());
+        other.localAddress(localAddress());
+        other.localPort(localPort());
+        other.useWebSockets(useWebSockets());
+        other.webSocketPath(webSocketPath());
+        other.webSocketHeaders().putAll(webSocketHeaders);
+        other.webSocketMaxFrameSize(webSocketMaxFrameSize());
+
+        return other;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionRedirectedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionRedirectedException.java
new file mode 100644
index 0000000..e393499
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionRedirectedException.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import java.net.URI;
+
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.impl.ClientRedirect;
+import org.apache.qpid.protonj2.types.transport.Open;
+
+/**
+ * A {@link ClientIOException} type that defines that the remote peer has requested that this
+ * connection be redirected to some alternative peer.  The redirect information can be obtained
+ * by calling the {@link ClientConnectionRedirectedException#getRedirectionURI()} method which
+ * return the URI of the peer the client is being redirect to.
+ */
+public class ClientConnectionRedirectedException extends ClientConnectionRemotelyClosedException {
+
+    private static final long serialVersionUID = 5872211116061710369L;
+
+    private final ClientRedirect redirect;
+
+    public ClientConnectionRedirectedException(String reason, ClientRedirect redirect, ErrorCondition condition) {
+        super(reason, condition);
+
+        this.redirect = redirect;
+    }
+
+    /**
+     * the host name of the remote peer where the {@link Connection} is being redirected.
+     * <p>
+     * This value should be used in the 'hostname' field of the {@link Open} frame, and
+     * during SASL negotiation (if used).  When using this client to reconnect this value
+     * would be assigned to the {@link ConnectionOptions#virtualHost(String)} value in the
+     * {@link ConnectionOptions} passed to the newly created {@link Connection}.
+     *
+     * @return the host name of the remote AMQP container to redirect to.
+     */
+    public String getHostname() {
+        return redirect.getHostname();
+    }
+
+    /**
+     * A network level host name that matches either the DNS hostname or IP address of the
+     * remote container.
+     *
+     * @return the network level host name value where the connection is being redirected.
+     */
+    public String getNetworkHost() {
+        return redirect.getNetworkHost();
+    }
+
+    /**
+     * A network level port value that should be used when redirecting this connection.
+     *
+     * @return the network port value where the connection is being redirected.
+     */
+    public int getPort() {
+        return redirect.getPort();
+    }
+
+    /**
+     * Returns the connection scheme that should be used when connecting to the remote
+     * host and port provided in this redirection.
+     *
+     * @return the connection scheme to use when redirecting to the provided host and port.
+     */
+    public String getScheme() {
+        return redirect.getScheme();
+    }
+
+    /**
+     * The path value that should be used when connecting to the provided host and port.
+     *
+     * @return the path value that should be used when redirecting to the provided host and port.
+     */
+    public String getPath() {
+        return redirect.getPath();
+    }
+
+    /**
+     * Attempt to construct a URI that represents the location where the redirect is
+     * sending the client {@link Connection}.
+     *
+     * @return the URI that represents the redirection.
+     *
+     * @throws Exception if an error occurs while converting the redirect into a URI.
+     */
+    public URI getRedirectionURI() throws Exception {
+        return redirect.toURI();
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionRemotelyClosedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionRemotelyClosedException.java
new file mode 100644
index 0000000..5962fd8
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionRemotelyClosedException.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Close;
+
+/**
+ * Exception thrown when the remote peer actively closes the {@link Connection} by sending
+ * and AMQP {@link Close} frame or when the IO layer is disconnected due to some other
+ * reason such as a security error or transient network error.
+ */
+public class ClientConnectionRemotelyClosedException extends ClientIOException {
+
+    private static final long serialVersionUID = 5728349272688210550L;
+
+    private final ErrorCondition condition;
+
+    public ClientConnectionRemotelyClosedException(String message) {
+        this(message, (ErrorCondition) null);
+    }
+
+    public ClientConnectionRemotelyClosedException(String message, Throwable cause) {
+        this(message, cause, null);
+    }
+
+    public ClientConnectionRemotelyClosedException(String message, ErrorCondition condition) {
+        super(message);
+        this.condition = condition;
+    }
+
+    public ClientConnectionRemotelyClosedException(String message, Throwable cause, ErrorCondition condition) {
+        super(message, cause);
+        this.condition = condition;
+    }
+
+    /**
+     * @return the {@link ErrorCondition} that was provided by the remote to describe the cause of the close.
+     */
+    public ErrorCondition getErrorCondition() {
+        return condition;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionSecurityException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionSecurityException.java
new file mode 100644
index 0000000..695161b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionSecurityException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+
+/**
+ * Connection level Security Exception used to indicate a security violation has occurred.
+ */
+public class ClientConnectionSecurityException extends ClientConnectionRemotelyClosedException {
+
+    private static final long serialVersionUID = -1895132556606592253L;
+
+    public ClientConnectionSecurityException(String message) {
+        super(message);
+    }
+
+    public ClientConnectionSecurityException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ClientConnectionSecurityException(String message, ErrorCondition errorCondition) {
+        super(message, errorCondition);
+    }
+
+    public ClientConnectionSecurityException(String message, Throwable cause, ErrorCondition errorCondition) {
+        super(message, cause, errorCondition);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionSecuritySaslException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionSecuritySaslException.java
new file mode 100644
index 0000000..112e387
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientConnectionSecuritySaslException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Security Exception used to indicate a security violation has occurred.
+ */
+public class ClientConnectionSecuritySaslException extends ClientConnectionSecurityException {
+
+    private static final long serialVersionUID = 313318720407251822L;
+
+    private boolean temporary;
+
+    public ClientConnectionSecuritySaslException(String message) {
+        this(message,false, null);
+    }
+
+    public ClientConnectionSecuritySaslException(String message, Throwable cause) {
+        this(message,false, cause);
+    }
+
+    public ClientConnectionSecuritySaslException(String message, boolean temporary) {
+        this(message, temporary, null);
+    }
+
+    public ClientConnectionSecuritySaslException(String message, boolean temporary, Throwable cause) {
+        super(message, cause);
+
+        this.temporary = temporary;
+    }
+
+    public boolean isSysTempFailure() {
+        return temporary;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryAbortedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryAbortedException.java
new file mode 100644
index 0000000..e2c6b5a
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryAbortedException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown in cases where an action was requested that cannot be performed because
+ * the delivery being operated on has been aborted by the remote sender.
+ */
+public class ClientDeliveryAbortedException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = 818288499075794863L;
+
+    public ClientDeliveryAbortedException(String message) {
+        super(message);
+    }
+
+    public ClientDeliveryAbortedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryIsPartialException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryIsPartialException.java
new file mode 100644
index 0000000..b33b081
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryIsPartialException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown in cases where an action was requested that cannot be performed because
+ * the delivery being operated on is only the partial Transfer payload.
+ */
+public class ClientDeliveryIsPartialException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = 3354944204399500545L;
+
+    public ClientDeliveryIsPartialException(String message) {
+        super(message);
+    }
+
+    public ClientDeliveryIsPartialException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryStateException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryStateException.java
new file mode 100644
index 0000000..b4f17f8
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientDeliveryStateException.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+
+/**
+ * Thrown from client API that deal with a {@link Delivery} or {@link Tracker} where the outcome
+ * that results from that API can affect whether the API call succeeded or failed.  Such a case might
+ * be that a sent message is awaiting a remote {@link Accepted} outcome but instead the remote sends
+ * a {@link Rejected} outcome.
+ */
+public class ClientDeliveryStateException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = -4699002536747966516L;
+
+    private final DeliveryState outcome;
+
+    public ClientDeliveryStateException(String message, DeliveryState outcome) {
+        super(message);
+        this.outcome = outcome;
+    }
+
+    public ClientDeliveryStateException(String message, Throwable cause, DeliveryState outcome) {
+        super(message, cause);
+        this.outcome = outcome;
+    }
+
+    /**
+     * @return the {@link DeliveryState} that defines the outcome returned from the remote peer.
+     */
+    public DeliveryState getOutcome() {
+        return this.outcome;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientException.java
new file mode 100644
index 0000000..68ab958
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.Connection;
+
+/**
+ * Represents a non-fatal exception that occurs from a Client {@link Connection}
+ * or one of its resources.  These error types can typically be recovered from
+ * without a full tear down and rebuild of the connection.  One example might be a
+ * failure to commit a transaction due to a forced roll back on the remote side
+ * of the connection.
+ */
+public class ClientException extends Exception {
+
+    private static final long serialVersionUID = -5094579928657311571L;
+
+    public ClientException(String message) {
+        super(message);
+    }
+
+    public ClientException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIOException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIOException.java
new file mode 100644
index 0000000..2ff9b07
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIOException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Exception type that is thrown when the provider has encountered an unrecoverable error.
+ */
+public class ClientIOException extends ClientException {
+
+    private static final long serialVersionUID = 7022573614211991693L;
+
+    public ClientIOException(String message) {
+        super(message);
+    }
+
+    public ClientIOException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIdleTimeoutException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIdleTimeoutException.java
new file mode 100644
index 0000000..fae0fd4
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIdleTimeoutException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when the Provider fails a connection due to idle timeout.
+ */
+public class ClientIdleTimeoutException extends ClientIOException {
+
+    private static final long serialVersionUID = 7925210908123213499L;
+
+    public ClientIdleTimeoutException(String message) {
+        super(message);
+    }
+
+    public ClientIdleTimeoutException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIllegalStateException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIllegalStateException.java
new file mode 100644
index 0000000..aff59d2
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientIllegalStateException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+public class ClientIllegalStateException extends ClientException {
+
+    private static final long serialVersionUID = -2188225056209312580L;
+
+    public ClientIllegalStateException(String message) {
+        super(message);
+    }
+
+    public ClientIllegalStateException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientLinkRedirectedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientLinkRedirectedException.java
new file mode 100644
index 0000000..1c9bb4c
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientLinkRedirectedException.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import java.net.URI;
+
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.impl.ClientRedirect;
+import org.apache.qpid.protonj2.types.transport.Open;
+
+/**
+ * A {@link ClientLinkRemotelyClosedException} type that defines that the remote peer has requested that
+ * this link be redirected to some alternative peer.  The redirect information can be obtained
+ * by calling the {@link ClientLinkRedirectedException#getRedirectionURI()} method which
+ * return the URI of the peer the client is being redirect to.  The address is also accessible
+ * for use in creating a new link after reconnection.
+ */
+public class ClientLinkRedirectedException extends ClientLinkRemotelyClosedException {
+
+    private static final long serialVersionUID = 5872211116061710369L;
+
+    private final ClientRedirect redirect;
+
+    public ClientLinkRedirectedException(String reason, ClientRedirect redirect, ErrorCondition condition) {
+        super(reason, condition);
+
+        this.redirect = redirect;
+    }
+
+    /**
+     * the host name of the remote peer where the {@link Sender} or {@link Receiver} is being
+     * redirected.
+     * <p>
+     * This value should be used in the 'hostname' field of the {@link Open} frame, and
+     * during SASL negotiation (if used).  When using this client to reconnect this value
+     * would be assigned to the {@link ConnectionOptions#virtualHost(String)} value in the
+     * {@link ConnectionOptions} passed to the newly created {@link Connection}.
+     *
+     * @return the host name of the remote AMQP container to redirect to.
+     */
+    public String getHostname() {
+        return redirect.getHostname();
+    }
+
+    /**
+     * A network level host name that matches either the DNS hostname or IP address of the
+     * remote container.
+     *
+     * @return the network level host name value where the connection is being redirected.
+     */
+    public String getNetworkHost() {
+        return redirect.getNetworkHost();
+    }
+
+    /**
+     * A network level port value that should be used when redirecting this connection.
+     *
+     * @return the network port value where the connection is being redirected.
+     */
+    public int getPort() {
+        return redirect.getPort();
+    }
+
+    /**
+     * Returns the connection scheme that should be used when connecting to the remote
+     * host and port provided in this redirection.
+     *
+     * @return the connection scheme to use when redirecting to the provided host and port.
+     */
+    public String getScheme() {
+        return redirect.getScheme();
+    }
+
+    /**
+     * The path value that should be used when connecting to the provided host and port.
+     *
+     * @return the path value that should be used when redirecting to the provided host and port.
+     */
+    public String getPath() {
+        return redirect.getPath();
+    }
+
+    /**
+     * The address value that should be used when connecting to the provided host and port and
+     * creating a new link instance as directed.
+     *
+     * @return the address value that should be used when redirecting to the provided host and port.
+     */
+    public String getAddress() {
+        return redirect.getAddress();
+    }
+
+    /**
+     * Attempt to construct a URI that represents the location where the redirect is
+     * sending the client {@link Sender} or {@link Receiver}.
+     *
+     * @return the URI that represents the redirection.
+     *
+     * @throws Exception if an error occurs while converting the redirect into a URI.
+     */
+    public URI getRedirectionURI() throws Exception {
+        return redirect.toURI();
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientLinkRemotelyClosedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientLinkRemotelyClosedException.java
new file mode 100644
index 0000000..73b34a6
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientLinkRemotelyClosedException.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.Sender;
+
+/**
+ * Root exception type for cases of remote closure or client created {@link Sender} or
+ * {@link Receiver}.
+ */
+public class ClientLinkRemotelyClosedException extends ClientResourceRemotelyClosedException {
+
+    private static final long serialVersionUID = 5601827103553513599L;
+
+    public ClientLinkRemotelyClosedException(String message) {
+        this(message, (ErrorCondition) null);
+    }
+
+    public ClientLinkRemotelyClosedException(String message, Throwable cause) {
+        this(message, cause, null);
+    }
+
+    public ClientLinkRemotelyClosedException(String message, ErrorCondition condition) {
+        super(message, condition);
+    }
+
+    public ClientLinkRemotelyClosedException(String message, Throwable cause, ErrorCondition condition) {
+        super(message, cause, condition);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientMessageFormatViolationException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientMessageFormatViolationException.java
new file mode 100644
index 0000000..1969637
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientMessageFormatViolationException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.Message;
+
+/**
+ * Exception thrown from {@link Message} instances when the body section specified
+ * violates the configure message format of the message that is being created.
+ */
+public class ClientMessageFormatViolationException extends ClientException {
+
+    private static final long serialVersionUID = -7731216779946825581L;
+
+    public ClientMessageFormatViolationException(String message) {
+        super(message);
+    }
+
+    public ClientMessageFormatViolationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientOperationTimedOutException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientOperationTimedOutException.java
new file mode 100644
index 0000000..8daf89e
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientOperationTimedOutException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Indicates that an operation in the provider timed out waiting for completion
+ */
+public class ClientOperationTimedOutException extends ClientException {
+
+    private static final long serialVersionUID = 4182665270566847828L;
+
+    public ClientOperationTimedOutException(String message) {
+        super(message);
+    }
+
+    public ClientOperationTimedOutException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientResourceRemotelyClosedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientResourceRemotelyClosedException.java
new file mode 100644
index 0000000..5e91b16
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientResourceRemotelyClosedException.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+
+/**
+ * Root exception type for cases of remote closure or client created resources other
+ * than the Client {@link Connection} which will throw exceptions rooted from the
+ * {@link ClientConnectionRemotelyClosedException} to indicate a fatal connection
+ * level error that requires a new connection to be created.
+ */
+public class ClientResourceRemotelyClosedException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = 5601827103553513599L;
+
+    private final ErrorCondition condition;
+
+    public ClientResourceRemotelyClosedException(String message) {
+        this(message, (ErrorCondition) null);
+    }
+
+    public ClientResourceRemotelyClosedException(String message, Throwable cause) {
+        this(message, cause, null);
+    }
+
+    public ClientResourceRemotelyClosedException(String message, ErrorCondition condition) {
+        super(message);
+        this.condition = condition;
+    }
+
+    public ClientResourceRemotelyClosedException(String message, Throwable cause, ErrorCondition condition) {
+        super(message, cause);
+        this.condition = condition;
+    }
+
+    /**
+     * @return the {@link ErrorCondition} that was provided by the remote to describe the cause of the close.
+     */
+    public ErrorCondition getErrorCondition() {
+        return condition;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientSendTimedOutException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientSendTimedOutException.java
new file mode 100644
index 0000000..bfeb84b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientSendTimedOutException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when a message send operation times out in the Provider layer.
+ */
+public class ClientSendTimedOutException extends ClientOperationTimedOutException {
+
+    private static final long serialVersionUID = 222325890763309867L;
+
+    public ClientSendTimedOutException(String reason) {
+        super(reason);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientSessionRemotelyClosedException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientSessionRemotelyClosedException.java
new file mode 100644
index 0000000..01688a3
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientSessionRemotelyClosedException.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Session;
+
+/**
+ * Root exception type for cases of remote closure or client created {@link Session}.
+ */
+public class ClientSessionRemotelyClosedException extends ClientResourceRemotelyClosedException {
+
+    private static final long serialVersionUID = 5601827103553513599L;
+
+    public ClientSessionRemotelyClosedException(String message) {
+        this(message, (ErrorCondition) null);
+    }
+
+    public ClientSessionRemotelyClosedException(String message, Throwable cause) {
+        this(message, cause, null);
+    }
+
+    public ClientSessionRemotelyClosedException(String message, ErrorCondition condition) {
+        super(message, condition);
+    }
+
+    public ClientSessionRemotelyClosedException(String message, Throwable cause, ErrorCondition condition) {
+        super(message, cause, condition);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionDeclarationException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionDeclarationException.java
new file mode 100644
index 0000000..2d01e3f
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionDeclarationException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when a transaction declaration fails or is rejected by the remote.
+ */
+public class ClientTransactionDeclarationException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = -5532644122754198664L;
+
+    public ClientTransactionDeclarationException(String message) {
+        super(message);
+    }
+
+    public ClientTransactionDeclarationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionInDoubtException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionInDoubtException.java
new file mode 100644
index 0000000..a55e6b0
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionInDoubtException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when a transaction operation fails and state is now unknown.
+ */
+public class ClientTransactionInDoubtException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = -5532644122754198664L;
+
+    public ClientTransactionInDoubtException(String message) {
+        super(message);
+    }
+
+    public ClientTransactionInDoubtException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionNotActiveException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionNotActiveException.java
new file mode 100644
index 0000000..95067c9
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionNotActiveException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when a client attempt to commit or roll-back when no transaction has been declared.
+ */
+public class ClientTransactionNotActiveException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = 7854401747821768051L;
+
+    public ClientTransactionNotActiveException(String message) {
+        super(message);
+    }
+
+    public ClientTransactionNotActiveException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionRolledBackException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionRolledBackException.java
new file mode 100644
index 0000000..d876dbc
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientTransactionRolledBackException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when a message send operation times out in the Provider layer.
+ */
+public class ClientTransactionRolledBackException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = 222325890763309867L;
+
+    public ClientTransactionRolledBackException(String message) {
+        super(message, null);
+    }
+
+    public ClientTransactionRolledBackException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientUnsupportedOperationException.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientUnsupportedOperationException.java
new file mode 100644
index 0000000..3d5206e
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/exceptions/ClientUnsupportedOperationException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.exceptions;
+
+/**
+ * Thrown when an action request is not supported through this provider.
+ */
+public class ClientUnsupportedOperationException extends ClientIllegalStateException {
+
+    private static final long serialVersionUID = -680156277783719903L;
+
+    public ClientUnsupportedOperationException(String message) {
+        super(message);
+    }
+
+    public ClientUnsupportedOperationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/AsyncResult.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/AsyncResult.java
new file mode 100644
index 0000000..5dc325f
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/AsyncResult.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * Defines a result interface for Asynchronous operations.
+ *
+ * @param <V> Type used to complete the future.
+ */
+public interface AsyncResult<V> {
+
+    /**
+     * If the operation fails this method is invoked with the Exception
+     * that caused the failure.
+     *
+     * @param result
+     *        The error that resulted in this asynchronous operation failing.
+     */
+    void failed(ClientException result);
+
+    /**
+     * If the operation succeeds the resulting value produced is set to null and
+     * the waiting parties are signaled.
+     *
+     * @param result
+     *      the object that completes the future.
+     */
+    void complete(V result);
+
+    /**
+     * Returns true if the AsyncResult has completed.  The task is considered complete
+     * regardless if it succeeded or failed.
+     *
+     * @return returns true if the asynchronous operation has completed.
+     */
+    boolean isComplete();
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/BalancedClientFuture.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/BalancedClientFuture.java
new file mode 100644
index 0000000..b35632b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/BalancedClientFuture.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A more balanced implementation of a ClientFuture that works better on some
+ * platforms such as windows where the thread park and atomic operations used by
+ * a more aggressive implementation could result in poor performance.
+ *
+ * @param <V> The type that result from completion of this Future
+ */
+public class BalancedClientFuture<V> extends ClientFuture<V> {
+
+    // Using a progressive wait strategy helps to avoid wait happening before
+    // completion and avoid using expensive thread signaling
+    private static final int SPIN_COUNT = 10;
+    private static final int YIELD_COUNT = 100;
+
+    public BalancedClientFuture() {
+        this(null);
+    }
+
+    public BalancedClientFuture(ClientSynchronization<V> synchronization) {
+        super(synchronization);
+    }
+
+    @Override
+    public V get(long amount, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        if (isNotComplete() && amount > 0) {
+            final long timeout = unit.toNanos(amount);
+            long maxParkNanos = timeout / 8;
+            maxParkNanos = maxParkNanos > 0 ? maxParkNanos : timeout;
+            final long startTime = System.nanoTime();
+            int idleCount = 0;
+
+            while (isNotComplete()) {
+                final long elapsed = System.nanoTime() - startTime;
+                final long diff = elapsed - timeout;
+
+                if (diff >= 0) {
+                    throw new TimeoutException("Timed out waiting for completion");
+                } else if (idleCount < SPIN_COUNT) {
+                    idleCount++;
+                } else if (idleCount < YIELD_COUNT) {
+                    Thread.yield();
+                    idleCount++;
+                } else {
+                    synchronized (this) {
+                        if (isComplete()) {
+                            break;
+                        } else if (getState() < COMPLETING) {
+                            waiting++;
+                            try {
+                                wait(-diff / 1000000, (int) (-diff % 1000000));
+                            } catch (InterruptedException e) {
+                                Thread.interrupted();
+                                throw e;
+                            } finally {
+                                waiting--;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            throw error;
+        } else {
+            return getResult();
+        }
+    }
+
+    @Override
+    public V get() throws InterruptedException, ExecutionException {
+        if (isNotComplete()) {
+            int idleCount = 0;
+
+            while (isNotComplete()) {
+                if (idleCount < SPIN_COUNT) {
+                    idleCount++;
+                } else if (idleCount < YIELD_COUNT) {
+                    Thread.yield();
+                    idleCount++;
+                } else {
+                    synchronized (this) {
+                        if (isComplete()) {
+                            break;
+                        } else if (getState() < COMPLETING) {
+                            waiting++;
+                            try {
+                                wait();
+                            } catch (InterruptedException e) {
+                                Thread.interrupted();
+                                throw e;
+                            } finally {
+                                waiting--;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            throw error;
+        } else {
+            return getResult();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientFuture.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientFuture.java
new file mode 100644
index 0000000..8a9eef9
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientFuture.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * Asynchronous Client Future class.
+ *
+ * @param <V> the eventual result type for this Future
+ */
+public abstract class ClientFuture<V> implements Future<V>, AsyncResult<V> {
+
+    protected final ClientSynchronization<V> synchronization;
+
+    // States used to track progress of this future
+    protected static final int INCOMPLETE = 0;
+    protected static final int COMPLETING = 1;
+    protected static final int SUCCESS = 2;
+    protected static final int FAILURE = 3;
+    protected static final int CANCELLED = 4;
+
+    @SuppressWarnings("rawtypes")
+    protected static final AtomicIntegerFieldUpdater<ClientFuture> STATE_FIELD_UPDATER =
+        AtomicIntegerFieldUpdater.newUpdater(ClientFuture.class,"state");
+
+    private volatile int state = INCOMPLETE;
+    protected ExecutionException error;
+    protected int waiting;
+    protected V result;
+
+    protected ClientFuture(ClientSynchronization<V> synchronization) {
+        this.synchronization = synchronization;
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        if (STATE_FIELD_UPDATER.compareAndSet(this, INCOMPLETE, COMPLETING)) {
+            STATE_FIELD_UPDATER.lazySet(this, CANCELLED);
+
+            synchronized(this) {
+                if (waiting > 0) {
+                    notifyAll();
+                }
+            }
+
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public boolean isFailed() {
+        return error != null;
+    }
+
+    public V getResult() {
+        return result;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return state > FAILURE;
+    }
+
+    @Override
+    public boolean isDone() {
+        return isComplete() || isCancelled() || isFailed();
+    }
+
+    @Override
+    public boolean isComplete() {
+        return state > COMPLETING;
+    }
+
+    protected boolean isNotComplete() {
+        return state <= COMPLETING;
+    }
+
+    /**
+     * @return the current {@link ClientFuture} state as if this call.
+     */
+    protected int getState() {
+        return state;
+    }
+
+    @Override
+    public void failed(ClientException result) {
+        Objects.requireNonNull(result, "Cannot fail the Future type without providing an error cause");
+
+        if (STATE_FIELD_UPDATER.compareAndSet(this, INCOMPLETE, COMPLETING)) {
+            error = new ExecutionException(result);
+
+            if (synchronization != null) {
+                try {
+                    synchronization.onPendingFailure(error);
+                } catch(Exception ignored) {}
+            }
+
+            STATE_FIELD_UPDATER.lazySet(this, FAILURE);
+
+            synchronized(this) {
+                if (waiting > 0) {
+                    notifyAll();
+                }
+            }
+        }
+    }
+
+    @Override
+    public void complete(V result) {
+        if (STATE_FIELD_UPDATER.compareAndSet(this, INCOMPLETE, COMPLETING)) {
+            this.result = result;
+
+            if (synchronization != null) {
+                try {
+                    synchronization.onPendingSuccess(result);
+                } catch(Exception ignored) {}
+            }
+
+            STATE_FIELD_UPDATER.lazySet(this, SUCCESS);
+
+            synchronized(this) {
+                if (waiting > 0) {
+                    notifyAll();
+                }
+            }
+        }
+    }
+
+    @Override
+    public abstract V get() throws InterruptedException, ExecutionException;
+
+    @Override
+    public abstract V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
+
+    /**
+     * TODO - Provide hook to run on the event loop to do whatever it means to cancel this task and
+     *        update the task state in a thread safe manner.
+     */
+    protected void tryCancelTask() {
+
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientFutureFactory.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientFutureFactory.java
new file mode 100644
index 0000000..5709ccd
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientFutureFactory.java
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import java.util.concurrent.Future;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * Factory for client future instances that will create specific versions based on
+ * configuration.
+ */
+public abstract class ClientFutureFactory {
+
+    private static final String OS_NAME = System.getProperty("os.name");
+    private static final String WINDOWS_OS_PREFIX = "Windows";
+    private static final boolean IS_WINDOWS = isOsNameMatch(OS_NAME, WINDOWS_OS_PREFIX);
+
+    public static final String CONSERVATIVE = "conservative";
+    public static final String BALANCED = "balanced";
+    public static final String PROGRESSIVE = "progressive";
+
+    /**
+     * Create a new ClientFutureFactory instance based on the given type name.
+     *
+     * @param futureType
+     * 		the future type whose factory should be returned.
+     *
+     * @return a new {@link ClientFutureFactory} that will be used to create the desired future types.
+     */
+    public static ClientFutureFactory create(final String futureType) {
+        if (futureType == null || futureType.isEmpty()) {
+            if (Runtime.getRuntime().availableProcessors() < 4) {
+                return new ConservativeProviderFutureFactory();
+            } else if (isWindows()) {
+                return new BalancedProviderFutureFactory();
+            } else {
+                return new ProgressiveProviderFutureFactory();
+            }
+        }
+
+        switch (futureType.toLowerCase()) {
+            case CONSERVATIVE:
+                return new ConservativeProviderFutureFactory();
+            case BALANCED:
+                return new BalancedProviderFutureFactory();
+            case PROGRESSIVE:
+                return new ProgressiveProviderFutureFactory();
+            default:
+                throw new IllegalArgumentException(
+                    "No ClientFuture implementation with name " + futureType + " found");
+        }
+    }
+
+    public static <T> Future<T> completedFuture(T result) {
+        BalancedClientFuture<T> future = new BalancedClientFuture<>();
+        future.complete(result);
+
+        return future;
+    }
+
+    /**
+     * @return a new ClientFuture instance.
+     *
+     * @param <V> the eventual result type for this Future
+     */
+    public abstract <V> ClientFuture<V> createFuture();
+
+    /**
+     * @param synchronization
+     * 		The {@link ClientSynchronization} to assign to the returned {@link ClientFuture}.
+     *
+     * @return a new ClientFuture instance.
+     *
+     * @param <V> the eventual result type for this Future
+     */
+    public abstract <V> ClientFuture<V> createFuture(ClientSynchronization<V> synchronization);
+
+    /**
+     * @return a ClientFuture that treats failures as success calls that simply complete the operation.
+     *
+     * @param <V> the eventual result type for this Future
+     */
+    public abstract <V> ClientFuture<V> createUnfailableFuture();
+
+    /**
+     * @param synchronization
+     *      The {@link ClientSynchronization} to assign to the returned {@link ClientFuture}.
+     *
+     * @return a ClientFuture that treats failures as success calls that simply complete the operation.
+     *
+     * @param <V> the eventual result type for this Future
+     */
+    public abstract <V> ClientFuture<V> createUnfailableFuture(ClientSynchronization<V> synchronization);
+
+    //----- Internal support methods -----------------------------------------//
+
+    private static boolean isWindows() {
+        return IS_WINDOWS;
+    }
+
+    private static boolean isOsNameMatch(final String currentOSName, final String osNamePrefix) {
+        if (currentOSName == null || currentOSName.isEmpty()) {
+            return false;
+        }
+
+        return currentOSName.startsWith(osNamePrefix);
+    }
+
+    //----- ClientFutureFactory implementation -----------------------------//
+
+    private static class ConservativeProviderFutureFactory extends ClientFutureFactory {
+
+        @Override
+        public <V> ClientFuture<V> createFuture() {
+            return new ConservativeClientFuture<>();
+        }
+
+        @Override
+        public <V> ClientFuture<V> createFuture(ClientSynchronization<V> synchronization) {
+            return new ConservativeClientFuture<>(synchronization);
+        }
+
+        @Override
+        public <V> ClientFuture<V> createUnfailableFuture() {
+            return createUnfailableFuture(null);
+        }
+
+        @Override
+        public <V> ClientFuture<V> createUnfailableFuture(ClientSynchronization<V> synchronization) {
+            return new ConservativeClientFuture<>(synchronization) {
+
+                @Override
+                public void failed(ClientException t) {
+                    this.complete(null);
+                }
+            };
+        }
+    }
+
+    private static class BalancedProviderFutureFactory extends ClientFutureFactory {
+
+        @Override
+        public <V> ClientFuture<V> createFuture() {
+            return new BalancedClientFuture<>();
+        }
+
+        @Override
+        public <V> ClientFuture<V> createFuture(ClientSynchronization<V> synchronization) {
+            return new BalancedClientFuture<>(synchronization);
+        }
+
+        @Override
+        public <V> ClientFuture<V> createUnfailableFuture() {
+            return createUnfailableFuture(null);
+        }
+
+        @Override
+        public <V> ClientFuture<V> createUnfailableFuture(ClientSynchronization<V> synchronization) {
+            return new BalancedClientFuture<>(synchronization) {
+
+                @Override
+                public void failed(ClientException t) {
+                    this.complete(null);
+                }
+            };
+        }
+    }
+
+    private static class ProgressiveProviderFutureFactory extends ClientFutureFactory {
+
+        @Override
+        public <V> ClientFuture<V> createFuture() {
+            return new ProgressiveClientFuture<>();
+        }
+
+        @Override
+        public <V> ClientFuture<V> createFuture(ClientSynchronization<V> synchronization) {
+            return new ProgressiveClientFuture<>(synchronization);
+        }
+
+        @Override
+        public <V> ClientFuture<V> createUnfailableFuture() {
+            return createUnfailableFuture(null);
+        }
+
+        @Override
+        public <V> ClientFuture<V> createUnfailableFuture(ClientSynchronization<V> synchronization) {
+            return new ProgressiveClientFuture<>(synchronization) {
+
+                @Override
+                public void failed(ClientException t) {
+                    this.complete(null);
+                }
+            };
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientSynchronization.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientSynchronization.java
new file mode 100644
index 0000000..d458946
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ClientSynchronization.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+/**
+ * Synchronization callback interface used to execute state updates
+ * or similar tasks in the thread context where the associated
+ * Future is managed.
+ *
+ * @param <V> The value that result when a {@link ClientFuture} succeeds
+ */
+public interface ClientSynchronization<V> {
+
+    void onPendingSuccess(V result);
+
+    void onPendingFailure(Throwable cause);
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ConservativeClientFuture.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ConservativeClientFuture.java
new file mode 100644
index 0000000..4384f85
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ConservativeClientFuture.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A more conservative implementation of a ClientFuture that is better on some
+ * platforms or resource constrained hardware where high CPU usage can be more
+ * counter productive than other variants that might spin or otherwise avoid
+ * entry into states requiring thread signaling.
+ *
+  * @param <V> The type that result from completion of this Future
+*/
+public class ConservativeClientFuture<V> extends ClientFuture<V> {
+
+    public ConservativeClientFuture() {
+        this(null);
+    }
+
+    public ConservativeClientFuture(ClientSynchronization<V> synchronization) {
+        super(synchronization);
+    }
+
+    @Override
+    public V get(long amount, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        if (isNotComplete() && amount > 0) {
+            final long timeout = unit.toNanos(amount);
+            long maxParkNanos = timeout / 8;
+            maxParkNanos = maxParkNanos > 0 ? maxParkNanos : timeout;
+            final long startTime = System.nanoTime();
+
+            while (isNotComplete()) {
+                final long elapsed = System.nanoTime() - startTime;
+                final long diff = elapsed - timeout;
+
+                if (diff >= 0) {
+                    throw new TimeoutException("Timed out waiting for completion");
+                }
+
+                synchronized (this) {
+                    if (isComplete()) {
+                        break;
+                    } else if (getState() < COMPLETING) {
+                        waiting++;
+                        try {
+                            wait(-diff / 1000000, (int) (-diff % 1000000));
+                        } catch (InterruptedException e) {
+                            Thread.interrupted();
+                            throw e;
+                        } finally {
+                            waiting--;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            throw error;
+        } else {
+            return getResult();
+        }
+    }
+
+    @Override
+    public V get() throws InterruptedException, ExecutionException {
+        while (isNotComplete()) {
+            synchronized (this) {
+                if (isComplete()) {
+                    break;
+                } else if (getState() < COMPLETING) {
+                    waiting++;
+                    try {
+                        wait();
+                    } catch (InterruptedException e) {
+                        Thread.interrupted();
+                        throw e;
+                    } finally {
+                        waiting--;
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            throw error;
+        } else {
+            return getResult();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/NoOpAsyncResult.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/NoOpAsyncResult.java
new file mode 100644
index 0000000..642c357
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/NoOpAsyncResult.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * Simple NoOp implementation used when the result of the operation does not matter.
+ */
+public class NoOpAsyncResult implements AsyncResult<Void> {
+
+    public final static NoOpAsyncResult INSTANCE = new NoOpAsyncResult();
+
+    @Override
+    public void failed(ClientException result) {
+
+    }
+
+    @Override
+    public void complete(Void result) {
+
+    }
+
+    @Override
+    public boolean isComplete() {
+        return true;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ProgressiveClientFuture.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ProgressiveClientFuture.java
new file mode 100644
index 0000000..e11c763
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/futures/ProgressiveClientFuture.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * An optimized version of a ClientFuture that makes use of spin waits and other
+ * methods of reacting to asynchronous completion in a more timely manner.
+ *
+ * @param <V> The type that result from completion of this Future
+ */
+public class ProgressiveClientFuture<V> extends ClientFuture<V> {
+
+    // Using a progressive wait strategy helps to avoid wait happening before
+    // completion and avoid using expensive thread signaling
+    private static final int SPIN_COUNT = 10;
+    private static final int YIELD_COUNT = 100;
+    private static final int TINY_PARK_COUNT = 1000;
+    private static final int TINY_PARK_NANOS = 1;
+    private static final int SMALL_PARK_COUNT = 101_000;
+    private static final int SMALL_PARK_NANOS = 10_000;
+
+    public ProgressiveClientFuture() {
+        this(null);
+    }
+
+    public ProgressiveClientFuture(ClientSynchronization<V> synchronization) {
+        super(synchronization);
+    }
+
+    @Override
+    public V get(long amount, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        if (isNotComplete() && amount > 0) {
+            final long timeout = unit.toNanos(amount);
+            long maxParkNanos = timeout / 8;
+            maxParkNanos = maxParkNanos > 0 ? maxParkNanos : timeout;
+            final long tinyParkNanos = Math.min(maxParkNanos, TINY_PARK_NANOS);
+            final long smallParkNanos = Math.min(maxParkNanos, SMALL_PARK_NANOS);
+            final long startTime = System.nanoTime();
+            int idleCount = 0;
+
+            while (isNotComplete()) {
+                final long elapsed = System.nanoTime() - startTime;
+                final long diff = elapsed - timeout;
+
+                if (diff >= 0) {
+                    throw new TimeoutException("Timed out waiting for completion");
+                } else if (idleCount < SPIN_COUNT) {
+                    idleCount++;
+                } else if (idleCount < YIELD_COUNT) {
+                    Thread.yield();
+                    idleCount++;
+                } else if (idleCount < TINY_PARK_COUNT) {
+                    LockSupport.parkNanos(tinyParkNanos);
+                    idleCount++;
+                } else if (idleCount < SMALL_PARK_COUNT) {
+                    LockSupport.parkNanos(smallParkNanos);
+                    idleCount++;
+                } else {
+                    synchronized (this) {
+                        if (isComplete()) {
+                            break;
+                        } else if (getState() < COMPLETING) {
+                            waiting++;
+                            try {
+                                wait(-diff / 1000000, (int) (-diff % 1000000));
+                            } catch (InterruptedException e) {
+                                Thread.interrupted();
+                                throw e;
+                            } finally {
+                                waiting--;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            throw error;
+        } else {
+            return getResult();
+        }
+    }
+
+    @Override
+    public V get() throws InterruptedException, ExecutionException {
+        if (isNotComplete()) {
+            int idleCount = 0;
+
+            while (isNotComplete()) {
+                if (idleCount < SPIN_COUNT) {
+                    idleCount++;
+                } else if (idleCount < YIELD_COUNT) {
+                    Thread.yield();
+                    idleCount++;
+                } else if (idleCount < TINY_PARK_COUNT) {
+                    LockSupport.parkNanos(TINY_PARK_NANOS);
+                    idleCount++;
+                } else if (idleCount < SMALL_PARK_COUNT) {
+                    LockSupport.parkNanos(SMALL_PARK_NANOS);
+                    idleCount++;
+                } else {
+                    synchronized (this) {
+                        if (isComplete()) {
+                            break;
+                        } else if (getState() < COMPLETING) {
+                            waiting++;
+                            try {
+                                wait();
+                            } catch (InterruptedException e) {
+                                Thread.interrupted();
+                                throw e;
+                            } finally {
+                                waiting--;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            throw error;
+        } else {
+            return getResult();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java
new file mode 100644
index 0000000..c743515
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java
@@ -0,0 +1,1068 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+import java.util.function.BiConsumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionEvent;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.DisconnectionEvent;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.StreamReceiverOptions;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderOptions;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecurityException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecuritySaslException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.client.futures.ClientFutureFactory;
+import org.apache.qpid.protonj2.client.transport.NettyIOContext;
+import org.apache.qpid.protonj2.client.transport.Transport;
+import org.apache.qpid.protonj2.client.util.ReconnectionURIPool;
+import org.apache.qpid.protonj2.client.util.TrackableThreadFactory;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.sasl.client.SaslAuthenticator;
+import org.apache.qpid.protonj2.engine.sasl.client.SaslCredentialsProvider;
+import org.apache.qpid.protonj2.engine.sasl.client.SaslMechanismSelector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link Connection} implementation that uses the Proton engine for AMQP protocol support.
+ */
+public class ClientConnection implements Connection {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientConnection.class);
+
+    private static final int UNLIMITED = -1;
+    private static final int UNDEFINED = -1;
+
+    // Future tracking of Closing. Closed. Failed state vs just simple boolean is intended here
+    // later on we may decide this is overly optimized.
+    private static final AtomicIntegerFieldUpdater<ClientConnection> CLOSED_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(ClientConnection.class, "closed");
+    private static final AtomicReferenceFieldUpdater<ClientConnection, ClientException> FAILURE_CAUSE_UPDATER =
+            AtomicReferenceFieldUpdater.newUpdater(ClientConnection.class, ClientException.class, "failureCause");
+
+    private final ClientInstance client;
+    private final ConnectionOptions options;
+    private final ClientConnectionCapabilities capabilities = new ClientConnectionCapabilities();
+    private final ClientFutureFactory futureFactory;
+    private final ClientSessionBuilder sessionBuilder;
+    private final ReconnectionURIPool reconnectPool = new ReconnectionURIPool();
+    private final NettyIOContext ioContext;
+    private final String connectionId;
+    private final ScheduledExecutorService executor;
+    private final Map<ClientFuture<?>, Object> requests = new ConcurrentHashMap<>();
+    private final ThreadPoolExecutor notifications;
+
+    private Engine engine;
+    private org.apache.qpid.protonj2.engine.Connection protonConnection;
+    private ClientSession connectionSession;
+    private ClientSender connectionSender;
+    private Transport transport;
+    private boolean autoFlush = true;
+    private ClientFuture<Connection> openFuture;
+    private ClientFuture<Connection> closeFuture;
+    private volatile int closed;
+    private volatile ClientException failureCause;
+    private long totalConnections;
+    private long reconnectAttempts;
+    private long nextReconnectDelay = -1;
+
+    /**
+     * Create a connection and define the initial configuration used to manage the
+     * connection to the remote.
+     *
+     * @param host
+     * 		the host that this connection is connecting to.
+     * @param port
+     * 		the port on the remote host where this connection attaches.
+     * @param client
+     *      the {@link Client} that this connection resides within.
+     * @param options
+     *      the connection options that configure this {@link Connection} instance.
+     */
+    ClientConnection(ClientInstance client, String host, int port, ConnectionOptions options) {
+        this.client = client;
+        this.options = options;
+        this.connectionId = client.nextConnectionId();
+        this.futureFactory = ClientFutureFactory.create(client.options().futureType());
+        this.openFuture = futureFactory.createFuture();
+        this.closeFuture = futureFactory.createFuture();
+        this.sessionBuilder = new ClientSessionBuilder(this);
+        this.ioContext = new NettyIOContext(options.transportOptions(),
+                                            options.sslOptions(),
+                                            "ClientConnection :(" + connectionId + "): I/O Thread");
+        this.executor = ioContext.eventLoop();
+
+        // This executor can be used for dispatching asynchronous tasks that might block or result
+        // in reentrant calls to this Connection that could block.
+        notifications = new ThreadPoolExecutor(1, 1, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(),
+            new TrackableThreadFactory("protonj2 Client Connection Executor: " + getId(), true));
+        notifications.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
+
+        try {
+            this.reconnectPool.add(new URI(null, null, host, port, null, null, null));
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("Invalid client remote host configured: " + host + ":" + port);
+        }
+
+        for (URI secondary : options.reconnectOptions().reconnectHosts()) {
+            this.reconnectPool.add(secondary);
+        }
+    }
+
+    @Override
+    public ClientInstance client() {
+        return client;
+    }
+
+    @Override
+    public Future<Connection> openFuture() {
+        return openFuture;
+    }
+
+    @Override
+    public void close() {
+        try {
+            doClose(null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void close(ErrorCondition error) {
+        try {
+            doClose(error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public Future<Connection> closeAsync() {
+        return doClose(null);
+    }
+
+    @Override
+    public Future<Connection> closeAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error supplied cannot be null");
+
+        return doClose(error);
+    }
+
+    private Future<Connection> doClose(ErrorCondition error) {
+        if (CLOSED_UPDATER.compareAndSet(this, 0, 1)) {
+            try {
+                if (!closeFuture.isDone()) {
+                    executor.execute(() -> {
+                        LOG.trace("Close requested for connection: {}", this);
+
+                        if (protonConnection.isLocallyOpen()) {
+                            protonConnection.setCondition(ClientErrorCondition.asProtonErrorCondition(error));
+
+                            try {
+                                protonConnection.close();
+                            } catch (Throwable ignored) {
+                                // Engine error handler will kick in if the write of Close fails
+                            }
+                        } else {
+                            engine.shutdown();
+                        }
+                    });
+                }
+            } catch (RejectedExecutionException rje) {
+                LOG.trace("Close task rejected from the event loop", rje);
+            } finally {
+                try {
+                    closeFuture.get();
+                } catch (InterruptedException | ExecutionException e) {
+                    // Ignore error as we are closed regardless
+                } finally {
+                    try {
+                        transport.close();
+                    } catch (Exception ignore) {}
+
+                    ioContext.shutdown();
+                }
+            }
+        }
+
+        return closeFuture;
+    }
+
+    @Override
+    public Session defaultSession() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Session> defaultSession = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                defaultSession.complete(lazyCreateConnectionSession());
+            } catch (Throwable error) {
+                defaultSession.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, defaultSession);
+    }
+
+    @Override
+    public Session openSession() throws ClientException {
+        return openSession(null);
+    }
+
+    @Override
+    public Session openSession(SessionOptions sessionOptions) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Session> createSession = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createSession.complete(sessionBuilder.session(sessionOptions).open());
+            } catch (Throwable error) {
+                createSession.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createSession);
+    }
+
+    @Override
+    public Receiver openReceiver(String address) throws ClientException {
+        return openReceiver(address, null);
+    }
+
+    @Override
+    public Receiver openReceiver(String address, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a receiver with a null address");
+        final ClientFuture<Receiver> createReceiver = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createReceiver.complete(lazyCreateConnectionSession().internalOpenReceiver(address, receiverOptions));
+            } catch (Throwable error) {
+                createReceiver.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createReceiver);
+    }
+
+    @Override
+    public Receiver openDurableReceiver(String address, String subscriptionName) throws ClientException {
+        return openDurableReceiver(address, subscriptionName, null);
+    }
+
+    @Override
+    public Receiver openDurableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a receiver with a null address");
+        final ClientFuture<Receiver> createReceiver = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createReceiver.complete(lazyCreateConnectionSession().internalOpenDurableReceiver(address, subscriptionName, receiverOptions));
+            } catch (Throwable error) {
+                createReceiver.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createReceiver);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver() throws ClientException {
+        return openDynamicReceiver(null, null);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties) throws ClientException {
+        return openDynamicReceiver(null, null);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(ReceiverOptions receiverOptions) throws ClientException {
+        return openDynamicReceiver(null, null);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Receiver> createReceiver = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createReceiver.complete(lazyCreateConnectionSession().internalOpenDynamicReceiver(dynamicNodeProperties, receiverOptions));
+            } catch (Throwable error) {
+                createReceiver.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createReceiver);
+    }
+
+    @Override
+    public StreamReceiver openStreamReceiver(String address) throws ClientException {
+        return openStreamReceiver(address, null);
+    }
+
+    @Override
+    public StreamReceiver openStreamReceiver(String address, StreamReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<StreamReceiver> createRequest = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                int sessionCapacity = StreamReceiverOptions.DEFAULT_READ_BUFFER_SIZE;
+                if (receiverOptions != null) {
+                    sessionCapacity = receiverOptions.readBufferSize() / 2;
+                }
+
+                // Session capacity cannot be smaller than one frame size so we adjust to the lower bound
+                sessionCapacity = (int) Math.max(sessionCapacity, protonConnection.getMaxFrameSize());
+
+                checkClosedOrFailed();
+                SessionOptions sessionOptions = new SessionOptions(sessionBuilder.getDefaultSessionOptions());
+                ClientStreamSession session = (ClientStreamSession) sessionBuilder.streamSession(sessionOptions.incomingCapacity(sessionCapacity)).open();
+                createRequest.complete(session.internalOpenStreamReceiver(address, receiverOptions));
+            } catch (Throwable error) {
+                createRequest.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createRequest);
+    }
+
+    @Override
+    public Sender defaultSender() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Sender> defaultSender = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                defaultSender.complete(lazyCreateConnectionSender());
+            } catch (Throwable error) {
+                defaultSender.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, defaultSender);
+    }
+
+    @Override
+    public Sender openSender(String address) throws ClientException {
+        return openSender(address, null);
+    }
+
+    @Override
+    public Sender openSender(String address, SenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a sender with a null address");
+        final ClientFuture<Sender> createSender = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createSender.complete(lazyCreateConnectionSession().internalOpenSender(address, senderOptions));
+            } catch (Throwable error) {
+                createSender.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createSender);
+    }
+
+    @Override
+    public Sender openAnonymousSender() throws ClientException {
+        return openAnonymousSender(null);
+    }
+
+    @Override
+    public Sender openAnonymousSender(SenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Sender> createRequest = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createRequest.complete(lazyCreateConnectionSession().internalOpenAnonymousSender(senderOptions));
+            } catch (Throwable error) {
+                createRequest.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createRequest);
+    }
+
+    @Override
+    public StreamSender openStreamSender(String address) throws ClientException {
+        return openStreamSender(address, null);
+    }
+
+    @Override
+    public StreamSender openStreamSender(String address, StreamSenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a sender with a null address");
+        final ClientFuture<StreamSender> createRequest = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                int sessionCapacity = StreamSenderOptions.DEFAULT_PENDING_WRITES_BUFFER_SIZE;
+                if (senderOptions != null) {
+                    sessionCapacity = senderOptions.pendingWritesBufferSize();
+                }
+
+                // Session capacity cannot be smaller than one frame size so we adjust to the lower bound
+                sessionCapacity = (int) Math.max(sessionCapacity, protonConnection.getMaxFrameSize());
+
+                checkClosedOrFailed();
+                SessionOptions sessionOptions = new SessionOptions(sessionBuilder.getDefaultSessionOptions());
+                ClientStreamSession session = (ClientStreamSession) sessionBuilder.streamSession(sessionOptions.outgoingCapacity(sessionCapacity)).open();
+                createRequest.complete(session.internalOpenStreamSender(address, senderOptions));
+            } catch (Throwable error) {
+                createRequest.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, createRequest);
+    }
+
+    @Override
+    public Tracker send(Message<?> message) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(message, "Cannot send a null message");
+        final ClientFuture<Sender> result = getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                result.complete(lazyCreateConnectionSender());
+            } catch (Throwable error) {
+                result.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return request(this, result).send(message);
+    }
+
+    @Override
+    public Map<String, Object> properties() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringKeyedMap(protonConnection.getRemoteProperties());
+    }
+
+    @Override
+    public String[] offeredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonConnection.getRemoteOfferedCapabilities());
+    }
+
+    @Override
+    public String[] desiredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonConnection.getRemoteDesiredCapabilities());
+    }
+
+    @Override
+    public String toString() {
+        return "ClientConnection:[" + getId() + "]";
+    }
+
+    //----- Internal API
+
+    String getId() {
+        return connectionId;
+    }
+
+    Engine getEngine() {
+        return engine;
+    }
+
+    ClientConnection connect() throws ClientException {
+        try {
+            final URI remoteHost = reconnectPool.getNext();
+
+            // Initial configuration validation happens here, if this step fails then the
+            // user most likely configured something incorrect or that violates some constraint
+            // like an invalid SASL mechanism etc.
+            initializeProtonResources(remoteHost.getHost());
+            scheduleReconnect(remoteHost.getHost(), remoteHost.getPort());
+
+            return this;
+        } catch (Exception ex) {
+            CLOSED_UPDATER.set(this, 1);
+            FAILURE_CAUSE_UPDATER.compareAndSet(this, null, ClientExceptionSupport.createOrPassthroughFatal(ex));
+            openFuture.failed(failureCause);
+            closeFuture.complete(this);
+            ioContext.shutdown();
+
+            throw failureCause;
+        }
+    }
+
+    boolean isClosed() {
+        return closed > 0;
+    }
+
+    ScheduledExecutorService getScheduler() {
+        return executor;
+    }
+
+    ClientFutureFactory getFutureFactory() {
+        return futureFactory;
+    }
+
+    ConnectionOptions getOptions() {
+        return options;
+    }
+
+    ClientConnectionCapabilities getCapabilities() {
+        return capabilities;
+    }
+
+    org.apache.qpid.protonj2.engine.Connection getProtonConnection() {
+        return protonConnection;
+    }
+
+    <T> T request(Object requestor, ClientFuture<T> request) throws ClientException {
+        requests.put(request, requestor);
+
+        try {
+            return request.get();
+        } catch (Throwable error) {
+            request.cancel(false);
+            throw ClientExceptionSupport.createNonFatalOrPassthrough(error);
+        } finally {
+            requests.remove(request);
+        }
+    }
+
+    void failAllPendingRequests(Object requestor, ClientException cause) {
+        requests.entrySet().removeIf(entry -> {
+            if (entry.getValue() == requestor) {
+                entry.getKey().failed(cause);
+                return true;
+            }
+
+            return false;
+        });
+    }
+
+    void autoFlushOff() {
+        autoFlush = false;
+    }
+
+    void autoFlushOn() {
+        autoFlush = true;
+    }
+
+    void flush() {
+        try {
+            transport.flush();
+        } catch (IOException e) {
+            LOG.debug("Error while flushing engine output to transport: ", e.getMessage());
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    //----- Private implementation events handlers and utility methods
+
+    private void handleLocalOpen(org.apache.qpid.protonj2.engine.Connection connection) {
+        connection.tickAuto(getScheduler());
+
+        if (options.openTimeout() > 0) {
+            executor.schedule(() -> {
+                if (!openFuture.isDone()) {
+                    // Ensure a close write is attempted and then force failure regardless
+                    // as we don't expect the remote to respond given it hasn't done so yet.
+                    try {
+                        connection.close();
+                    } catch (Throwable ignore) {}
+
+                    connection.getEngine().engineFailed(new ClientOperationTimedOutException(
+                        "Connection Open timed out waiting for remote to open"));
+                }
+            }, options.openTimeout(), TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void handleLocalClose(org.apache.qpid.protonj2.engine.Connection connection) {
+        if (connection.isRemotelyClosed()) {
+            final ClientException failureCause;
+
+            if (engine.connection().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(connection.getRemoteCondition());
+            } else {
+                failureCause = new ClientConnectionRemotelyClosedException("Unknown error led to connection disconnect");
+            }
+
+            try {
+                connection.getEngine().engineFailed(failureCause);
+            } catch (Throwable ignore) {
+            }
+        } else if (!engine.isShutdown() || !engine.isFailed()) {
+            // Ensure engine gets shut down and future completed if remote doesn't respond.
+            executor.schedule(() -> {
+                try {
+                    connection.getEngine().shutdown();
+                } catch (Throwable ignore) {
+                }
+            }, options.closeTimeout(), TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void handleRemoteOpen(org.apache.qpid.protonj2.engine.Connection connection) {
+        connectionEstablished();
+        capabilities.determineCapabilities(connection);
+
+        if (totalConnections == 1) {
+            submitConnectionEvent(options.connectedHandler(), transport.getHost(), transport.getPort(), null);
+        } else {
+            submitConnectionEvent(options.reconnectedHandler(), transport.getHost(), transport.getPort(), null);
+        }
+
+        openFuture.complete(this);
+    }
+
+    private void handleRemotecClose(org.apache.qpid.protonj2.engine.Connection connection) {
+        // When the connection is already locally closed this implies the application requested
+        // a close of this connection so this is normal, if not then the remote is closing for
+        // some reason and we should react as if the connection has failed which we will determine
+        // in the local close handler based on state.
+        if (connection.isLocallyClosed()) {
+            try {
+                connection.getEngine().shutdown();
+            } catch (Throwable ignore) {
+                LOG.warn("Unexpected exception thrown from engine shutdown: ", ignore);
+            }
+        } else {
+            try {
+                connection.close();
+            } catch (Throwable ignored) {
+                // Engine handlers will ensure we close down if not already locally closed.
+            }
+        }
+    }
+
+    private void handleEngineOutput(ProtonBuffer output, Runnable ioComplete) {
+        try {
+            if (autoFlush) {
+                transport.writeAndFlush(output, ioComplete);
+            } else {
+                transport.write(output, ioComplete);
+            }
+        } catch (IOException e) {
+            LOG.debug("Error while writing engine output to transport: ", e.getMessage());
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /*
+     * When an engine fails we check if we can reconnect or not and act accordingly.
+     */
+    private void handleEngineFailure(Engine engine) {
+        final ClientIOException failureCause;
+
+        if (engine.connection().getRemoteCondition() != null) {
+            failureCause = ClientExceptionSupport.convertToConnectionClosedException(engine.connection().getRemoteCondition());
+        } else if (engine.failureCause() != null) {
+            failureCause = ClientExceptionSupport.convertToConnectionClosedException(engine.failureCause());
+        } else {
+            failureCause = new ClientConnectionRemotelyClosedException("Unknown error led to connection disconnect");
+        }
+
+        LOG.trace("Engine reports failure with error: {}", failureCause.getMessage());
+
+        if (isReconnectAllowed(failureCause)) {
+            submitDisconnectionEvent(options.interruptedHandler(), transport.getHost(), transport.getPort(), failureCause);
+
+            // Initial configuration validation happens here, if this step fails then the
+            // user most likely configured something incorrect or that violates some constraint
+            // like an invalid SASL mechanism etc.
+            try {
+                final URI remoteHost = reconnectPool.getNext();
+
+                initializeProtonResources(remoteHost.getHost());
+                scheduleReconnect(remoteHost.getHost(), remoteHost.getPort());
+            } catch (ClientException initError) {
+                failConnection(ClientExceptionSupport.createOrPassthroughFatal(initError));
+            } finally {
+                engine.shutdown();
+            }
+        } else {
+            failConnection(failureCause);
+        }
+    }
+
+    /*
+     * Handle normal engine shutdown which should only happen when the connection is closed
+     * by the user, all other cases should lead to engine failed event first which will deal
+     * with reconnect cases and avoid this event unless reconnect cannot proceed.
+     */
+    private void handleEngineShutdown(Engine engine) {
+        // Only handle this on normal shutdown failure will perform its own controlled shutdown
+        // and or reconnection logic which this method should avoid interfering with.
+        if (engine.failureCause() == null) {
+            try {
+                protonConnection.close();
+            } catch (Exception ignore) {
+            }
+
+            try {
+                transport.close();
+            } catch (Exception ignored) {}
+
+            client.unregisterConnection(this);
+
+            openFuture.complete(this);
+            closeFuture.complete(this);
+        }
+    }
+
+    private void submitConnectionEvent(BiConsumer<Connection, ConnectionEvent> handler, String host, int port, ClientIOException cause) {
+        if (handler != null) {
+            try {
+                notifications.submit(() -> {
+                    try {
+                        handler.accept(this, new ConnectionEvent(host, port));
+                    } catch (Exception ex) {
+                        LOG.trace("User supplied connection life-cycle event handler threw: ", ex);
+                    }
+                });
+            } catch (Exception ex) {
+                LOG.trace("Error thrown while attempting to submit event notification ", ex);
+            }
+        }
+    }
+
+    private void submitDisconnectionEvent(BiConsumer<Connection, DisconnectionEvent> handler, String host, int port, ClientIOException cause) {
+        if (handler != null) {
+            try {
+                notifications.submit(() -> {
+                    try {
+                        handler.accept(this, new DisconnectionEvent(host, port, cause));
+                    } catch (Exception ex) {
+                        LOG.trace("User supplied disconnection life-cycle event handler threw: ", ex);
+                    }
+                });
+            } catch (Exception ex) {
+                LOG.trace("Error thrown while attempting to submit event notification ", ex);
+            }
+        }
+    }
+
+    private void failConnection(ClientIOException failureCause) {
+        FAILURE_CAUSE_UPDATER.compareAndSet(this, null, failureCause);
+
+        try {
+            protonConnection.close();
+        } catch (Exception ignore) {}
+
+        try {
+            engine.shutdown();
+        } catch (Exception ignore) {}
+
+        openFuture.failed(failureCause);
+        closeFuture.complete(this);
+
+        submitDisconnectionEvent(options.disconnectedHandler(), transport.getHost(), transport.getPort(), failureCause);
+    }
+
+    private Engine configureEngineSaslSupport() {
+        if (options.saslOptions().saslEnabled()) {
+            SaslMechanismSelector mechSelector =
+                new SaslMechanismSelector(ClientConversionSupport.toSymbolSet(options.saslOptions().allowedMechanisms()));
+
+            engine.saslDriver().client().setListener(new SaslAuthenticator(mechSelector, new SaslCredentialsProvider() {
+
+                @Override
+                public String vhost() {
+                    return options.virtualHost();
+                }
+
+                @Override
+                public String username() {
+                    return options.user();
+                }
+
+                @Override
+                public String password() {
+                    return options.password();
+                }
+
+                @Override
+                public Principal localPrincipal() {
+                    return transport.getLocalPrincipal();
+                }
+            }));
+        }
+
+        return engine;
+    }
+
+    private void initializeProtonResources(String host) throws ClientException {
+        if (options.saslOptions().saslEnabled()) {
+            engine = EngineFactory.PROTON.createEngine();
+        } else {
+            engine = EngineFactory.PROTON.createNonSaslEngine();
+        }
+
+        if (options.traceFrames()) {
+            engine.configuration().setTraceFrames(true);
+            if (!engine.configuration().isTraceFrames()) {
+                LOG.info("Connection frame tracing was enabled but protocol engine does not support it");
+            }
+        }
+
+        engine.outputHandler(this::handleEngineOutput)
+              .shutdownHandler(this::handleEngineShutdown)
+              .errorHandler(this::handleEngineFailure);
+
+        protonConnection = engine.connection();
+
+        if (client.containerId() != null) {
+            protonConnection.setContainerId(client.containerId());
+        }
+
+        protonConnection.setLinkedResource(this);
+        protonConnection.setChannelMax(options.channelMax());
+        protonConnection.setMaxFrameSize(options.maxFrameSize());
+        protonConnection.setHostname(host);
+        protonConnection.setIdleTimeout((int) options.idleTimeout());
+        protonConnection.setOfferedCapabilities(ClientConversionSupport.toSymbolArray(options.offeredCapabilities()));
+        protonConnection.setDesiredCapabilities(ClientConversionSupport.toSymbolArray(options.desiredCapabilities()));
+        protonConnection.setProperties(ClientConversionSupport.toSymbolKeyedMap(options.properties()));
+        protonConnection.localOpenHandler(this::handleLocalOpen)
+                        .localCloseHandler(this::handleLocalClose)
+                        .openHandler(this::handleRemoteOpen)
+                        .closeHandler(this::handleRemotecClose);
+
+        configureEngineSaslSupport();
+    }
+
+    private ClientSession lazyCreateConnectionSession() throws ClientException {
+        if (connectionSession == null) {
+            connectionSession = sessionBuilder.session(null).open();
+        }
+
+        return connectionSession;
+    }
+
+    private Sender lazyCreateConnectionSender() throws ClientException {
+        if (connectionSender == null) {
+            if (openFuture.isComplete()) {
+                checkAnonymousRelaySupported();
+            }
+
+            connectionSender = lazyCreateConnectionSession().internalOpenAnonymousSender(null);
+            connectionSender.remotelyClosedHandler((sender) -> {
+                try {
+                    sender.closeAsync();
+                } catch (Throwable ignore) {}
+
+                // Clear the old closed sender, a lazy create needs to construct a new sender.
+                connectionSender = null;
+            });
+        }
+
+        return connectionSender;
+    }
+
+    void checkAnonymousRelaySupported() throws ClientUnsupportedOperationException {
+        if (!capabilities.anonymousRelaySupported()) {
+            throw new ClientUnsupportedOperationException("Anonymous relay support not available from this connection");
+        }
+    }
+
+    protected void checkClosedOrFailed() throws ClientException {
+        if (closed > 0) {
+            throw new ClientIllegalStateException("The Connection was explicity closed", failureCause);
+        } else if (failureCause != null) {
+            throw failureCause;
+        }
+    }
+
+    private void waitForOpenToComplete() throws ClientException {
+        if (!openFuture.isComplete() || openFuture.isFailed()) {
+            try {
+                openFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                Thread.interrupted();
+                if (failureCause != null) {
+                    throw failureCause;
+                } else {
+                    throw ClientExceptionSupport.createNonFatalOrPassthrough(e.getCause());
+                }
+            }
+        }
+    }
+
+    //----- Reconnection related internal API
+
+    private void attemptConnection(String host, int port) {
+        try {
+            reconnectAttempts++;
+            transport = ioContext.newTransport();
+            LOG.trace("Attempting connection to remote {}:{}", host, port);
+            transport.connect(host, port, new ClientTransportListener(engine));
+        } catch (Throwable error) {
+            engine.engineFailed(ClientExceptionSupport.createOrPassthroughFatal(error));
+        }
+    }
+
+    private void scheduleReconnect(String host, int port) {
+        // Warn of ongoing connection attempts if configured.
+        int warnInterval = options.reconnectOptions().warnAfterReconnectAttempts();
+        if (reconnectAttempts > 0 && warnInterval > 0 && (reconnectAttempts % warnInterval) == 0) {
+            LOG.warn("Failed to connect after: {} attempt(s) continuing to retry.", reconnectAttempts);
+        }
+
+        // If no connection recovery required then we have never fully connected to a remote
+        // so we proceed down the connect with one immediate connection attempt and then follow
+        // on delayed attempts based on configuration.
+        if (totalConnections == 0) {
+            if (reconnectAttempts == 0) {
+                LOG.trace("Initial connect attempt will be performed immediately");
+                executor.execute(() -> attemptConnection(host, port));
+            } else {
+                long delay = nextReconnectDelay();
+                LOG.trace("Next connect attempt will be in {} milliseconds", delay);
+                executor.schedule(() -> attemptConnection(host, port), delay, TimeUnit.MILLISECONDS);
+            }
+        } else if (reconnectAttempts == 0) {
+            LOG.trace("Initial reconnect attempt will be performed immediately");
+            executor.execute(() -> attemptConnection(host, port));
+        } else {
+            long delay = nextReconnectDelay();
+            LOG.trace("Next reconnect attempt will be in {} milliseconds", delay);
+            executor.schedule(() -> attemptConnection(host, port), delay, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void connectionEstablished() {
+        totalConnections++;
+        nextReconnectDelay = -1;
+        reconnectAttempts = 0;
+    }
+
+    private boolean isLimitExceeded() {
+        int reconnectLimit = reconnectAttemptLimit();
+        if (reconnectLimit != UNLIMITED && reconnectAttempts >= reconnectLimit) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean isReconnectAllowed(ClientException cause) {
+        if (options.reconnectOptions().reconnectEnabled() && !isClosed()) {
+            // If a connection attempts fail due to Security errors than we abort
+            // reconnection as there is a configuration issue and we want to avoid
+            // a spinning reconnect cycle that can never complete.
+            if (isStoppageCause(cause)) {
+                return false;
+            }
+
+            return !isLimitExceeded();
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isStoppageCause(ClientException cause) {
+        if (cause instanceof ClientConnectionSecuritySaslException) {
+            ClientConnectionSecuritySaslException saslFailure = (ClientConnectionSecuritySaslException) cause;
+            return !saslFailure.isSysTempFailure();
+        } else if (cause instanceof ClientConnectionSecurityException ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private int reconnectAttemptLimit() {
+        int maxReconnectValue = options.reconnectOptions().maxReconnectAttempts();
+        if (totalConnections == 0 && options.reconnectOptions().maxInitialConnectionAttempts() != UNDEFINED) {
+            // If this is the first connection attempt and a specific startup retry limit
+            // is configured then use it, otherwise use the main reconnect limit
+            maxReconnectValue = options.reconnectOptions().maxInitialConnectionAttempts();
+        }
+
+        return maxReconnectValue;
+    }
+
+    private long nextReconnectDelay() {
+        if (nextReconnectDelay == UNDEFINED) {
+            nextReconnectDelay = options.reconnectOptions().reconnectDelay();
+        }
+
+        if (options.reconnectOptions().useReconnectBackOff() && reconnectAttempts > 1) {
+            // Exponential increment of reconnect delay.
+            nextReconnectDelay *= options.reconnectOptions().reconnectBackOffMultiplier();
+            if (nextReconnectDelay > options.reconnectOptions().maxReconnectDelay()) {
+                nextReconnectDelay = options.reconnectOptions().maxReconnectDelay();
+            }
+        }
+
+        return nextReconnectDelay;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnectionCapabilities.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnectionCapabilities.java
new file mode 100644
index 0000000..e3bb624
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnectionCapabilities.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Tracks available known capabilities for the connection to allow the client
+ * to know what features are supported on the current connection.
+ */
+public class ClientConnectionCapabilities {
+
+    private boolean anonymousRelaySupported;
+    private boolean delayedDeliverySupported;
+
+    /**
+     * @return true this the client requested and the remote answered that anonymous relay is supported.
+     */
+    public boolean anonymousRelaySupported() {
+        return this.anonymousRelaySupported;
+    }
+
+    public boolean deliveryDelaySupported() {
+        return this.delayedDeliverySupported;
+    }
+
+    @SuppressWarnings("unchecked")
+    ClientConnectionCapabilities determineCapabilities(Connection connection) {
+        final Symbol[] desired = connection.getDesiredCapabilities();
+        final Symbol[] offered = connection.getRemoteOfferedCapabilities();
+
+        final List<Symbol> offeredSymbols = offered != null ? Arrays.asList(offered) : Collections.EMPTY_LIST;
+        final List<Symbol> desiredSymbols = desired != null ? Arrays.asList(desired) : Collections.EMPTY_LIST;
+
+        anonymousRelaySupported = checkAnonymousRelaySupported(desiredSymbols, offeredSymbols);
+        delayedDeliverySupported = checkDeliveryRelaySupported(desiredSymbols, offeredSymbols);
+
+        return this;
+    }
+
+    private boolean checkAnonymousRelaySupported(List<Symbol> desired, List<Symbol> offered) {
+        return offered.contains(ClientConstants.ANONYMOUS_RELAY);
+    }
+
+    private boolean checkDeliveryRelaySupported(List<Symbol> desired, List<Symbol> offered) {
+        return offered.contains(ClientConstants.DELAYED_DELIVERY);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConstants.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConstants.java
new file mode 100644
index 0000000..8b33fba
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConstants.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Constants that are used throughout the client implementation.
+ */
+public class ClientConstants {
+
+    // Symbols used to announce connection error information
+    public static final Symbol CONNECTION_OPEN_FAILED = Symbol.valueOf("amqp:connection-establishment-failed");
+    public static final Symbol INVALID_FIELD = Symbol.valueOf("invalid-field");
+    public static final Symbol CONTAINER_ID = Symbol.valueOf("container-id");
+
+    // Symbols used for connection capabilities
+    public static final Symbol SOLE_CONNECTION_CAPABILITY = Symbol.valueOf("sole-connection-for-container");
+    public static final Symbol ANONYMOUS_RELAY = Symbol.valueOf("ANONYMOUS-RELAY");
+    public static final Symbol DELAYED_DELIVERY = Symbol.valueOf("DELAYED_DELIVERY");
+    public static final Symbol SHARED_SUBS = Symbol.valueOf("SHARED-SUBS");
+
+    // Symbols used to announce connection and link redirect ErrorCondition 'info'
+    public static final Symbol ADDRESS = Symbol.valueOf("address");
+    public static final Symbol PATH = Symbol.valueOf("path");
+    public static final Symbol SCHEME = Symbol.valueOf("scheme");
+    public static final Symbol PORT = Symbol.valueOf("port");
+    public static final Symbol NETWORK_HOST = Symbol.valueOf("network-host");
+    public static final Symbol OPEN_HOSTNAME = Symbol.valueOf("hostname");
+
+    // Symbols used for receivers.
+    public static final Symbol COPY = Symbol.getSymbol("copy");
+    public static final Symbol MOVE = Symbol.getSymbol("move");
+    public static final Symbol SHARED = Symbol.valueOf("shared");
+    public static final Symbol GLOBAL = Symbol.valueOf("global");
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConversionSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConversionSupport.java
new file mode 100644
index 0000000..7e284f7
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConversionSupport.java
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.DistributionMode;
+import org.apache.qpid.protonj2.client.DurabilityMode;
+import org.apache.qpid.protonj2.client.ExpiryPolicy;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+
+/**
+ * Utilities used by various classes in the Client core
+ */
+abstract class ClientConversionSupport {
+
+    public static Symbol[] toSymbolArray(String[] stringArray) {
+        Symbol[] result = null;
+
+        if (stringArray != null) {
+            result = new Symbol[stringArray.length];
+            for (int i = 0; i < stringArray.length; ++i) {
+                result[i] = Symbol.valueOf(stringArray[i]);
+            }
+        }
+
+        return result;
+    }
+
+    public static String[] toStringArray(Symbol[] symbolArray) {
+        String[] result = null;
+
+        if (symbolArray != null) {
+            result = new String[symbolArray.length];
+            for (int i = 0; i < symbolArray.length; ++i) {
+                result[i] = symbolArray[i].toString();
+            }
+        }
+
+        return result;
+    }
+
+    public static Map<Symbol, Object> toSymbolKeyedMap(Map<String, ?> stringsMap) {
+        final Map<Symbol, Object> result;
+
+        if (stringsMap != null) {
+            result = new HashMap<>(stringsMap.size());
+            stringsMap.forEach((key, value) -> {
+                result.put(Symbol.valueOf(key), value);
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Map<String, Object> toStringKeyedMap(Map<Symbol, ?> symbolMap) {
+        Map<String, Object> result;
+
+        if (symbolMap != null) {
+            result = new LinkedHashMap<>(symbolMap.size());
+            symbolMap.forEach((key, value) -> {
+                result.put(key.toString(), value);
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Symbol[] toSymbolArray(Set<String> stringsSet) {
+        final Symbol[] result;
+
+        if (stringsSet != null) {
+            result = new Symbol[stringsSet.size()];
+            int index = 0;
+            for (String entry : stringsSet) {
+                result[index++] = Symbol.valueOf(entry);
+            }
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Set<Symbol> toSymbolSet(Set<String> stringsSet) {
+        final Set<Symbol> result;
+
+        if (stringsSet != null) {
+            result = new LinkedHashSet<>(stringsSet.size());
+            stringsSet.forEach((entry) -> {
+                result.add(Symbol.valueOf(entry));
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Set<String> toStringSet(Symbol[] symbols) {
+        Set<String> result;
+
+        if (symbols != null) {
+            result = new LinkedHashSet<>(symbols.length);
+            for (Symbol symbol : symbols) {
+                result.add(symbol.toString());
+            }
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Symbol[] outcomesToSymbols(DeliveryState.Type[] outcomes) {
+        Symbol[] result = null;
+
+        if (outcomes != null) {
+            result = new Symbol[outcomes.length];
+            for (int i = 0; i < outcomes.length; ++i) {
+                result[i] = outcomeToSymbol(outcomes[i]);
+            }
+        }
+
+        return result;
+    }
+
+    public static DeliveryState.Type[] symbolsToOutcomes(Symbol[] outcomes) {
+        DeliveryState.Type[] result = null;
+
+        if (outcomes != null) {
+            result = new DeliveryState.Type[outcomes.length];
+            for (int i = 0; i < outcomes.length; ++i) {
+                result[i] = symbolToOutcome(outcomes[i]);
+            }
+        }
+
+        return result;
+    }
+
+    public static Symbol outcomeToSymbol(DeliveryState.Type outcome) {
+        if (outcome == null) {
+            return null;
+        }
+
+        switch (outcome) {
+            case ACCEPTED:
+                return Accepted.DESCRIPTOR_SYMBOL;
+            case REJECTED:
+                return Rejected.DESCRIPTOR_SYMBOL;
+            case RELEASED:
+                return Released.DESCRIPTOR_SYMBOL;
+            case MODIFIED:
+                return Modified.DESCRIPTOR_SYMBOL;
+            default:
+                throw new IllegalArgumentException("DeliveryState.Type " + outcome + " cannot be applied as an outcome");
+        }
+    }
+
+    public static DeliveryState.Type symbolToOutcome(Symbol outcome) {
+        if (outcome == null) {
+            return null;
+        } else if (outcome.equals(Accepted.DESCRIPTOR_SYMBOL)) {
+            return DeliveryState.Type.ACCEPTED;
+        } else if (outcome.equals(Rejected.DESCRIPTOR_SYMBOL)) {
+            return DeliveryState.Type.REJECTED;
+        } else if (outcome.equals(Released.DESCRIPTOR_SYMBOL)) {
+            return DeliveryState.Type.RELEASED;
+        } else if (outcome.equals(Modified.DESCRIPTOR_SYMBOL)) {
+            return DeliveryState.Type.MODIFIED;
+        } else {
+            throw new IllegalArgumentException("Cannot convert Symbol: " + outcome + " to a DeliveryState.Type outcome");
+        }
+    }
+
+    public static Symbol asProtonType(DistributionMode mode) {
+        Symbol result = null;
+
+        if (mode != null) {
+            switch (mode) {
+                case COPY:
+                    result = ClientConstants.COPY;
+                    break;
+                case MOVE:
+                    result = ClientConstants.MOVE;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return result;
+    }
+
+    public static TerminusDurability asProtonType(DurabilityMode mode) {
+        TerminusDurability result = null;
+
+        if (mode != null) {
+            switch (mode) {
+                case CONFIGURATION:
+                    result = TerminusDurability.CONFIGURATION;
+                    break;
+                case NONE:
+                    result = TerminusDurability.NONE;
+                    break;
+                case UNSETTLED_STATE:
+                    result = TerminusDurability.UNSETTLED_STATE;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return result;
+    }
+
+    public static TerminusExpiryPolicy asProtonType(ExpiryPolicy policy) {
+        TerminusExpiryPolicy result = null;
+
+        if (policy != null) {
+            switch (policy) {
+                case CONNECTION_CLOSE:
+                    result = TerminusExpiryPolicy.CONNECTION_CLOSE;
+                    break;
+                case LINK_CLOSE:
+                    result = TerminusExpiryPolicy.LINK_DETACH;
+                    break;
+                case NEVER:
+                    result = TerminusExpiryPolicy.NEVER;
+                    break;
+                case SESSION_CLOSE:
+                    result = TerminusExpiryPolicy.SESSION_END;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientDelivery.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientDelivery.java
new file mode 100644
index 0000000..e790c1b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientDelivery.java
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Client inbound delivery object.
+ */
+public final class ClientDelivery implements Delivery {
+
+    private final ClientReceiver receiver;
+    private final IncomingDelivery delivery;
+    private final ProtonBuffer payload;
+
+    private DeliveryAnnotations deliveryAnnotations;
+    private Message<?> cachedMessage;
+    private InputStream rawInputStream;
+
+    /**
+     * Creates a new client delivery object linked to the given {@link IncomingDelivery}
+     * instance.
+     *
+     * @param receiver
+     *      The {@link Receiver} that processed this delivery.
+     * @param delivery
+     *      The proton incoming delivery that backs this client delivery facade.
+     */
+    ClientDelivery(ClientReceiver receiver, IncomingDelivery delivery) {
+        this.receiver = receiver;
+        this.delivery = delivery;
+        this.delivery.setLinkedResource(this);
+        this.payload = delivery.readAll();
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <E> Message<E> message() throws ClientException {
+        if (rawInputStream != null) {
+            throw new ClientIllegalStateException("Cannot access Delivery Annotations API after requesting an InputStream");
+        }
+
+        Message<E> message = (Message<E>) cachedMessage;
+        if (message == null && payload.isReadable()) {
+            message = (Message<E>)(cachedMessage = ClientMessageSupport.decodeMessage(payload, this::deliveryAnnotations));
+        }
+
+        return message;
+    }
+
+    @Override
+    public InputStream rawInputStream() throws ClientException {
+        if (cachedMessage != null) {
+            throw new ClientIllegalStateException("Cannot access Delivery InputStream API after requesting an Message");
+        }
+
+        if (rawInputStream == null) {
+            rawInputStream = new ProtonBufferInputStream(payload);
+        }
+
+        return rawInputStream;
+    }
+
+    @Override
+    public Map<String, Object> annotations() throws ClientException {
+        message();
+
+        if (deliveryAnnotations != null && deliveryAnnotations.getValue() != null) {
+            return StringUtils.toStringKeyedMap(deliveryAnnotations.getValue());
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Delivery accept() throws ClientException {
+        receiver.disposition(delivery, Accepted.getInstance(), true);
+        return this;
+    }
+
+    @Override
+    public Delivery release() throws ClientException {
+        receiver.disposition(delivery, Released.getInstance(), true);
+        return this;
+    }
+
+    @Override
+    public Delivery reject(String condition, String description) throws ClientException {
+        receiver.disposition(delivery, new Rejected().setError(new ErrorCondition(condition, description)), true);
+        return this;
+    }
+
+    @Override
+    public Delivery modified(boolean deliveryFailed, boolean undeliverableHere) throws ClientException {
+        receiver.disposition(delivery, new Modified().setDeliveryFailed(deliveryFailed).setUndeliverableHere(undeliverableHere), true);
+        return this;
+    }
+
+    @Override
+    public Delivery disposition(DeliveryState state, boolean settle) throws ClientException {
+        receiver.disposition(delivery, ClientDeliveryState.asProtonType(state), settle);
+        return this;
+    }
+
+    @Override
+    public Delivery settle() throws ClientException {
+        receiver.disposition(delivery, null, true);
+        return this;
+    }
+
+    @Override
+    public DeliveryState state() {
+        return ClientDeliveryState.fromProtonType(delivery.getState());
+    }
+
+    @Override
+    public DeliveryState remoteState() {
+        return ClientDeliveryState.fromProtonType(delivery.getRemoteState());
+    }
+
+    @Override
+    public boolean remoteSettled() {
+        return delivery.isRemotelySettled();
+    }
+
+    @Override
+    public int messageFormat() {
+        return delivery.getMessageFormat();
+    }
+
+    @Override
+    public Receiver receiver() {
+        return receiver;
+    }
+
+    @Override
+    public boolean settled() {
+        return delivery.isSettled();
+    }
+
+    //----- Internal API not meant to be used from outside the client package.
+
+    IncomingDelivery protonDelivery() {
+        return delivery;
+    }
+
+    void deliveryAnnotations(DeliveryAnnotations deliveryAnnotations) {
+        this.deliveryAnnotations = deliveryAnnotations;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientDeliveryState.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientDeliveryState.java
new file mode 100644
index 0000000..a9e0d90
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientDeliveryState.java
@@ -0,0 +1,259 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Client internal implementation of a DeliveryState type.
+ */
+public abstract class ClientDeliveryState implements DeliveryState {
+
+    //----- Abstract methods for nested types
+
+    /**
+     * Returns the Proton version of the specific {@link org.apache.qpid.protonj2.amqp.transport.DeliveryState} that
+     * this type represents.
+     *
+     * @return the Proton state object that this type maps to.
+     */
+    abstract org.apache.qpid.protonj2.types.transport.DeliveryState getProtonDeliveryState();
+
+    //----- Create Delivery State from Proton instance
+
+    static DeliveryState fromProtonType(org.apache.qpid.protonj2.types.messaging.Outcome outcome) {
+        if (outcome == null) {
+            return null;
+        }
+
+        if (outcome instanceof Accepted) {
+            return ClientAccepted.getInstance();
+        } else if (outcome instanceof Released) {
+            return ClientReleased.getInstance();
+        } else if (outcome instanceof Rejected) {
+            return ClientRejected.fromProtonType((Rejected) outcome);
+        } else if (outcome instanceof Modified) {
+            return ClientModified.fromProtonType((Modified) outcome);
+        }
+
+        throw new IllegalArgumentException("Cannot map to unknown Proton Outcome to a DeliveryStateType: " + outcome);
+    }
+
+    static DeliveryState fromProtonType(org.apache.qpid.protonj2.types.transport.DeliveryState state) {
+        if (state == null) {
+            return null;
+        }
+
+        switch (state.getType()) {
+            case Accepted:
+                return ClientAccepted.getInstance();
+            case Released:
+                return ClientReleased.getInstance();
+            case Rejected:
+                return ClientRejected.fromProtonType((Rejected) state);
+            case Modified:
+                return ClientModified.fromProtonType((Modified) state);
+            case Transactional:
+                return ClientTransactional.fromProtonType((TransactionalState) state);
+            default:
+                throw new IllegalArgumentException("Cannot map to unknown Proton Delivery State type");
+        }
+    }
+
+    static DeliveryState.Type fromOutcomeSymbol(Symbol outcome) {
+        if (outcome == null) {
+            return null;
+        }
+
+        try {
+            return DeliveryState.Type.valueOf(outcome.toString().toUpperCase());
+        } catch (Throwable error) {
+            throw new IllegalArgumentException("Cannot map outcome name to unknown Proton DeliveryState.Type");
+        }
+    }
+
+    static org.apache.qpid.protonj2.types.transport.DeliveryState asProtonType(DeliveryState state) {
+        if (state == null) {
+            return null;
+        } else if (state instanceof ClientDeliveryState) {
+            return ((ClientDeliveryState) state).getProtonDeliveryState();
+        } else {
+            switch (state.getType()) {
+                case ACCEPTED:
+                    return Accepted.getInstance();
+                case RELEASED:
+                    return Released.getInstance();
+                case REJECTED:
+                    return new Rejected(); // TODO - How do we aggregate the different values into one DeliveryState Object
+                case MODIFIED:
+                    return new Modified(); // TODO - How do we aggregate the different values into one DeliveryState Object
+                case TRANSACTIONAL:
+                    throw new IllegalArgumentException("Cannot manually enlist delivery in AMQP Transactions");
+                default:
+                    throw new UnsupportedOperationException("Client does not support the given Delivery State type: " + state.getType());
+            }
+        }
+    }
+
+    //----- Delivery State implementations
+
+    public static class ClientAccepted extends ClientDeliveryState {
+
+        private static final ClientAccepted INSTANCE = new ClientAccepted();
+
+        @Override
+        public Type getType() {
+            return Type.ACCEPTED;
+        }
+
+        @Override
+        org.apache.qpid.protonj2.types.transport.DeliveryState getProtonDeliveryState() {
+            return Accepted.getInstance();
+        }
+
+        public static ClientAccepted getInstance() {
+            return INSTANCE;
+        }
+    }
+
+    public static class ClientReleased extends ClientDeliveryState {
+
+        private static final ClientReleased INSTANCE = new ClientReleased();
+
+        @Override
+        public Type getType() {
+            return Type.RELEASED;
+        }
+
+        @Override
+        org.apache.qpid.protonj2.types.transport.DeliveryState getProtonDeliveryState() {
+            return Released.getInstance();
+        }
+
+        public static ClientReleased getInstance() {
+            return INSTANCE;
+        }
+    }
+
+    public static class ClientRejected extends ClientDeliveryState {
+
+        private final Rejected rejected = new Rejected();
+
+        ClientRejected(Rejected rejected) {
+            if (rejected.getError() != null) {
+                rejected.setError(rejected.getError().copy());
+            }
+        }
+
+        public ClientRejected(String condition, String description) {
+            if (condition != null || description != null) {
+                rejected.setError(new ErrorCondition(Symbol.valueOf(condition), description));
+            }
+        }
+
+        public ClientRejected(String condition, String description, Map<String, Object> info) {
+            if (condition != null || description != null) {
+                rejected.setError(new ErrorCondition(
+                    Symbol.valueOf(condition), description, ClientConversionSupport.toSymbolKeyedMap(info)));
+            }
+        }
+
+        @Override
+        public Type getType() {
+            return Type.RELEASED;
+        }
+
+        @Override
+        org.apache.qpid.protonj2.types.transport.DeliveryState getProtonDeliveryState() {
+            return rejected;
+        }
+
+        public static ClientRejected fromProtonType(Rejected rejected) {
+            return new ClientRejected(rejected);
+        }
+    }
+
+    public static class ClientModified extends ClientDeliveryState {
+
+        private final Modified modified = new Modified();
+
+        ClientModified(Modified modified) {
+            this.modified.setDeliveryFailed(modified.isDeliveryFailed());
+            this.modified.setUndeliverableHere(modified.isUndeliverableHere());
+            this.modified.setMessageAnnotations(new LinkedHashMap<>(modified.getMessageAnnotations()));
+        }
+
+        public ClientModified(boolean failed, boolean undeliverable) {
+            modified.setDeliveryFailed(failed);
+            modified.setUndeliverableHere(undeliverable);
+        }
+
+        public ClientModified(boolean failed, boolean undeliverable, Map<String, Object> annotations) {
+            modified.setDeliveryFailed(failed);
+            modified.setUndeliverableHere(undeliverable);
+            modified.setMessageAnnotations(ClientConversionSupport.toSymbolKeyedMap(annotations));
+        }
+
+        @Override
+        public Type getType() {
+            return Type.MODIFIED;
+        }
+
+        @Override
+        org.apache.qpid.protonj2.types.transport.DeliveryState getProtonDeliveryState() {
+            return modified;
+        }
+
+        public static ClientModified fromProtonType(Modified modified) {
+            return new ClientModified(modified);
+        }
+    }
+
+    public static class ClientTransactional extends ClientDeliveryState {
+
+        private final TransactionalState txnState = new TransactionalState();
+
+        ClientTransactional(TransactionalState txnState) {
+            this.txnState.setOutcome(txnState.getOutcome());
+            this.txnState.setTxnId(txnState.getTxnId().copy());
+        }
+
+        @Override
+        public Type getType() {
+            return Type.TRANSACTIONAL;
+        }
+
+        @Override
+        org.apache.qpid.protonj2.types.transport.DeliveryState getProtonDeliveryState() {
+            return txnState;
+        }
+
+        public static ClientTransactional fromProtonType(TransactionalState txnState) {
+            return new ClientTransactional(txnState);
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientErrorCondition.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientErrorCondition.java
new file mode 100644
index 0000000..1b9915e
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientErrorCondition.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Client implementation of the {@link ErrorCondition} type that wraps a
+ * Proton specific AMQP {@link org.apache.qpid.protonj2.amqp.transport.ErrorCondition}.
+ */
+public final class ClientErrorCondition implements ErrorCondition {
+
+    private final org.apache.qpid.protonj2.types.transport.ErrorCondition error;
+
+    public ClientErrorCondition(ErrorCondition condition) {
+        Objects.requireNonNull(condition, "The error condition value cannot be null");
+
+        error = new org.apache.qpid.protonj2.types.transport.ErrorCondition(
+            Symbol.valueOf(condition.condition()), condition.description(), ClientConversionSupport.toSymbolKeyedMap(condition.info()));
+    }
+
+    public ClientErrorCondition(String condition, String description, Map<String, Object> info) {
+        Objects.requireNonNull(condition, "The error condition value cannot be null");
+
+        error = new org.apache.qpid.protonj2.types.transport.ErrorCondition(
+            Symbol.valueOf(condition), description, ClientConversionSupport.toSymbolKeyedMap(info));
+    }
+
+    ClientErrorCondition(org.apache.qpid.protonj2.types.transport.ErrorCondition condition) {
+        Objects.requireNonNull(condition, "The error condition value cannot be null");
+
+        error = condition;
+    }
+
+    @Override
+    public String condition() {
+        return error.getCondition().toString();
+    }
+
+    @Override
+    public String description() {
+        return error.getDescription();
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Map<String, Object> info() {
+        return error.getInfo() == null ? Collections.EMPTY_MAP : ClientConversionSupport.toStringKeyedMap(error.getInfo());
+    }
+
+    //----- Internal methods used by Client resources
+
+    org.apache.qpid.protonj2.types.transport.ErrorCondition getProtonErrorCondition() {
+        return error;
+    }
+
+    static org.apache.qpid.protonj2.types.transport.ErrorCondition asProtonErrorCondition(ErrorCondition condition) {
+        if (condition == null) {
+            return null;
+        } else if (condition instanceof ClientErrorCondition) {
+            return ((ClientErrorCondition) condition).getProtonErrorCondition();
+        } else {
+            return new ClientErrorCondition(condition).getProtonErrorCondition();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientExceptionSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientExceptionSupport.java
new file mode 100644
index 0000000..aa48e11
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientExceptionSupport.java
@@ -0,0 +1,359 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRedirectedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecurityException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecuritySaslException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientLinkRedirectedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientLinkRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientResourceRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientSessionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionRolledBackException;
+import org.apache.qpid.protonj2.engine.sasl.SaslSystemException;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transactions.TransactionErrors;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.LinkError;
+
+public class ClientExceptionSupport {
+
+    /**
+     * Checks the given cause to determine if it's already an ProviderIOException type and
+     * if not creates a new ProviderIOException to wrap it.
+     *
+     * @param cause
+     *        The initiating exception that should be cast or wrapped.
+     *
+     * @return an ProviderIOException instance.
+     */
+    public static ClientIOException createOrPassthroughFatal(Throwable cause) {
+        if (cause instanceof ClientIOException) {
+            return (ClientIOException) cause;
+        }
+
+        if (cause.getCause() instanceof ClientIOException) {
+            return (ClientIOException) cause.getCause();
+        }
+
+        String message = cause.getMessage();
+        if (message == null || message.length() == 0) {
+            message = cause.toString();
+        }
+
+        return new ClientIOException(message, cause);
+    }
+
+    /**
+     * Checks the given cause to determine if it's already an ProviderException type and
+     * if not creates a new ProviderException to wrap it.  If the inbound exception is a
+     * fatal type then it will pass through this method untouched to preserve the fatal
+     * status of the error.
+     *
+     * @param cause
+     *        The initiating exception that should be cast or wrapped.
+     *
+     * @return an ProviderException instance.
+     */
+    public static ClientException createNonFatalOrPassthrough(Throwable cause) {
+        if (cause instanceof ClientException) {
+            return (ClientException) cause;
+        }
+
+        if (cause.getCause() instanceof ClientException) {
+            return (ClientException) cause.getCause();
+        }
+
+        String message = cause.getMessage();
+        if (message == null || message.length() == 0) {
+            message = cause.toString();
+        }
+
+        if (cause instanceof TimeoutException) {
+            return new ClientOperationTimedOutException(message, cause);
+        } else if (cause instanceof IllegalStateException) {
+            return new ClientIllegalStateException(message, cause);
+        } else {
+            return new ClientException(message, cause);
+        }
+    }
+
+    /**
+     * Given an ErrorCondition instance create a new Exception that best matches
+     * the error type that indicates the connection creation failed for some reason.
+     *
+     * @param errorCondition
+     *      The ErrorCondition returned from the remote peer.
+     *
+     * @return a new Exception instance that best matches the ErrorCondition value.
+     */
+    public static ClientConnectionRemotelyClosedException convertToConnectionClosedException(ErrorCondition errorCondition) {
+        final ClientConnectionRemotelyClosedException remoteError;
+
+        if (errorCondition != null && errorCondition.getCondition() != null) {
+            Symbol error = errorCondition.getCondition();
+            String message = extractErrorMessage(errorCondition);
+
+            if (error.equals(AmqpError.UNAUTHORIZED_ACCESS)) {
+                remoteError = new ClientConnectionSecurityException(message, new ClientErrorCondition(errorCondition));
+            } else if (error.equals(ConnectionError.REDIRECT)) {
+                remoteError = createConnectionRedirectException(error, message, errorCondition);
+            } else {
+                remoteError = new ClientConnectionRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+            }
+        } else {
+            remoteError = new ClientConnectionRemotelyClosedException("Unknown error from remote peer");
+        }
+
+        return remoteError;
+    }
+
+    /**
+     * Given an ErrorCondition instance create a new Exception that best matches
+     * the error type that indicates the connection creation failed for some reason.
+     *
+     * @param cause
+     *        The initiating exception that should be cast or wrapped.
+     *
+     * @return a new Exception instance that best matches the ErrorCondition value.
+     */
+    public static ClientConnectionRemotelyClosedException convertToConnectionClosedException(Throwable cause) {
+        ClientConnectionRemotelyClosedException remoteError = null;
+
+        if (cause instanceof ClientConnectionRemotelyClosedException) {
+            remoteError = (ClientConnectionRemotelyClosedException) cause;
+        } else if (cause instanceof SaslSystemException) {
+            remoteError = new ClientConnectionSecuritySaslException(
+                cause.getMessage(), !((SaslSystemException) cause).isPermanent(), cause);
+        } else if (cause instanceof SaslException) {
+            remoteError = new ClientConnectionSecuritySaslException(cause.getMessage(), cause);
+        } else {
+            remoteError = new ClientConnectionRemotelyClosedException(cause.getMessage(), cause);
+        }
+
+        return remoteError;
+    }
+
+    /**
+     * Given an ErrorCondition instance create a new Exception that best matches
+     * the error type that indicates the connection creation failed for some reason.
+     *
+     * @param errorCondition
+     *      The ErrorCondition returned from the remote peer.
+     *
+     * @return a new Exception instance that best matches the ErrorCondition value.
+     */
+    public static ClientSessionRemotelyClosedException convertToSessionClosedException(ErrorCondition errorCondition) {
+        final ClientSessionRemotelyClosedException remoteError;
+
+        if (errorCondition != null && errorCondition.getCondition() != null) {
+            String message = extractErrorMessage(errorCondition);
+            if (message == null) {
+                message = "Session remotely closed without explanation";
+            }
+
+            remoteError = new ClientSessionRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+        } else {
+            remoteError = new ClientSessionRemotelyClosedException("Session remotely closed without explanation");
+        }
+
+        return remoteError;
+    }
+
+    /**
+     * Given an ErrorCondition instance create a new Exception that best matches
+     * the error type that indicates the connection creation failed for some reason.
+     *
+     * @param errorCondition
+     *      The ErrorCondition returned from the remote peer.
+     * @param defaultMessage
+     *      The message to use if the remote provided no condition for the closure
+     *
+     * @return a new Exception instance that best matches the ErrorCondition value.
+     */
+    public static ClientLinkRemotelyClosedException convertToLinkClosedException(ErrorCondition errorCondition, String defaultMessage) {
+        final ClientLinkRemotelyClosedException remoteError;
+
+        if (errorCondition != null && errorCondition.getCondition() != null) {
+            String message = extractErrorMessage(errorCondition);
+            Symbol error = errorCondition.getCondition();
+
+            if (message == null) {
+                message = defaultMessage;
+            }
+
+            if (error.equals(LinkError.REDIRECT)) {
+                remoteError = createLinkRedirectException(error, message, errorCondition);
+            } else {
+                remoteError = new ClientLinkRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+            }
+        } else {
+            remoteError = new ClientLinkRemotelyClosedException(defaultMessage);
+        }
+
+        return remoteError;
+    }
+
+    /**
+     * Given an ErrorCondition instance create a new Exception that best matches
+     * the error type that indicates a non-fatal error usually at the link level
+     * such as link closed remotely or link create failed due to security access
+     * issues.
+     *
+     * @param errorCondition
+     *      The ErrorCondition returned from the remote peer.
+     *
+     * @return a new Exception instance that best matches the ErrorCondition value.
+     */
+    public static ClientException convertToNonFatalException(ErrorCondition errorCondition) {
+        final ClientException remoteError;
+
+        if (errorCondition != null && errorCondition.getCondition() != null) {
+            Symbol error = errorCondition.getCondition();
+            String message = extractErrorMessage(errorCondition);
+
+            if (error.equals(AmqpError.RESOURCE_LIMIT_EXCEEDED)) {
+                remoteError = new ClientResourceRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+            } else if (error.equals(AmqpError.NOT_FOUND)) {
+                remoteError = new ClientResourceRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+            } else if (error.equals(LinkError.DETACH_FORCED)) {
+                remoteError = new ClientResourceRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+            } else if (error.equals(LinkError.REDIRECT)) {
+                remoteError = createLinkRedirectException(error, message, errorCondition);
+            } else if (error.equals(AmqpError.RESOURCE_DELETED)) {
+                remoteError = new ClientResourceRemotelyClosedException(message, new ClientErrorCondition(errorCondition));
+            } else if (error.equals(TransactionErrors.TRANSACTION_ROLLBACK)) {
+                remoteError = new ClientTransactionRolledBackException(message);
+            } else {
+                remoteError = new ClientException(message);
+            }
+        } else {
+            remoteError = new ClientException("Unknown error from remote peer");
+        }
+
+        return remoteError;
+    }
+
+    /**
+     * Attempt to read and return the embedded error message in the given ErrorCondition
+     * object.  If no message can be extracted a generic message is returned.
+     *
+     * @param errorCondition
+     *      The ErrorCondition to extract the error message from.
+     *
+     * @return an error message extracted from the given ErrorCondition.
+     */
+    public static String extractErrorMessage(ErrorCondition errorCondition) {
+        String message = "Received error from remote peer without description";
+        if (errorCondition != null) {
+            if (errorCondition.getDescription() != null && !errorCondition.getDescription().isEmpty()) {
+                message = errorCondition.getDescription();
+            }
+
+            Symbol condition = errorCondition.getCondition();
+            if (condition != null) {
+                message = message + " [condition = " + condition + "]";
+            }
+        }
+
+        return message;
+    }
+
+    /**
+     * When a connection redirect type exception is received this method is called to create the
+     * appropriate redirect exception type containing the error details needed.
+     *
+     * @param error
+     *        the Symbol that defines the redirection error type.
+     * @param message
+     *        the basic error message that should used or amended for the returned exception.
+     * @param condition
+     *        the ErrorCondition that describes the redirection.
+     *
+     * @return an Exception that captures the details of the redirection error.
+     */
+    public static ClientConnectionRemotelyClosedException createConnectionRedirectException(Symbol error, String message, ErrorCondition condition) {
+        ClientConnectionRemotelyClosedException result;
+        Map<?, ?> info = condition.getInfo();
+
+        if (info == null) {
+            result = new ClientConnectionRemotelyClosedException(
+                message + " : Redirection information not set.", new ClientErrorCondition(condition));
+        } else {
+            @SuppressWarnings("unchecked")
+            ClientRedirect redirect = new ClientRedirect((Map<Symbol, Object>) info);
+
+            try {
+                result = new ClientConnectionRedirectedException(
+                    message, redirect.validate(), new ClientErrorCondition(condition));
+            } catch (Exception ex) {
+                result = new ClientConnectionRemotelyClosedException(
+                    message + " : " + ex.getMessage(), new ClientErrorCondition(condition));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * When a link redirect type exception is received this method is called to create the
+     * appropriate redirect exception type containing the error details needed.
+     *
+     * @param error
+     *        the Symbol that defines the redirection error type.
+     * @param message
+     *        the basic error message that should used or amended for the returned exception.
+     * @param condition
+     *        the ErrorCondition that describes the redirection.
+     *
+     * @return an Exception that captures the details of the redirection error.
+     */
+    public static ClientLinkRemotelyClosedException createLinkRedirectException(Symbol error, String message, ErrorCondition condition) {
+        ClientLinkRemotelyClosedException result;
+        Map<?, ?> info = condition.getInfo();
+
+        if (info == null) {
+            result = new ClientLinkRemotelyClosedException(
+                message + " : Redirection information not set.", new ClientErrorCondition(condition));
+        } else {
+            @SuppressWarnings("unchecked")
+            ClientRedirect redirect = new ClientRedirect((Map<Symbol, Object>) info);
+
+            try {
+                result = new ClientLinkRedirectedException(
+                    message, redirect.validate(), new ClientErrorCondition(condition));
+            } catch (Exception ex) {
+                result = new ClientLinkRemotelyClosedException(
+                    message + " : " + ex.getMessage(), new ClientErrorCondition(condition));
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientInstance.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientInstance.java
new file mode 100644
index 0000000..3688d82
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientInstance.java
@@ -0,0 +1,178 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.ClientOptions;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.client.futures.ClientFutureFactory;
+import org.apache.qpid.protonj2.client.util.IdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Container of {@link Connection} instances that are all created with the same
+ * container parent and therefore share the same container Id.
+ */
+public final class ClientInstance implements Client {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientInstance.class);
+
+    private static final IdGenerator CONTAINER_ID_GENERATOR = new IdGenerator();
+    private static final ClientFutureFactory FUTURES = ClientFutureFactory.create(ClientFutureFactory.CONSERVATIVE);
+
+    private final AtomicInteger CONNECTION_COUNTER = new AtomicInteger();
+    private final ClientOptions options;
+    private final ConnectionOptions defaultConnectionOptions = new ConnectionOptions();
+    private final Map<String, ClientConnection> connections = new HashMap<>();
+    private final String clientUniqueId = CONTAINER_ID_GENERATOR.generateId();
+    private final ClientFuture<Client> closedFuture = FUTURES.createFuture();
+
+    private volatile boolean closed;
+
+    public static ClientInstance create() {
+        return new ClientInstance(new ClientOptions());
+    }
+
+    public static ClientInstance create(ClientOptions options) {
+        Objects.requireNonNull(options, "Client options must be non-null");
+        Objects.requireNonNull(options.id(), "User supplied container Id must be non-null");
+
+        return new ClientInstance(new ClientOptions(options));
+    }
+
+    /**
+     * @param options
+     *      The container options to use to configure this container instance.
+     */
+    ClientInstance(ClientOptions options) {
+        this.options = options;
+    }
+
+    @SuppressWarnings("resource")
+    @Override
+    public synchronized Connection connect(String host, int port) throws ClientException {
+        checkClosed();
+        return addConnection(new ClientConnection(this, host, port, defaultConnectionOptions).connect());
+    }
+
+    @SuppressWarnings("resource")
+    @Override
+    public synchronized Connection connect(String host, int port, ConnectionOptions options) throws ClientException {
+        checkClosed();
+        return addConnection(new ClientConnection(this, host, port, new ConnectionOptions(options)).connect());
+    }
+
+    @SuppressWarnings("resource")
+    @Override
+    public synchronized Connection connect(String host) throws ClientException {
+        checkClosed();
+        return addConnection(new ClientConnection(this, host, -1, defaultConnectionOptions).connect());
+    }
+
+    @SuppressWarnings("resource")
+    @Override
+    public synchronized Connection connect(String host, ConnectionOptions options) throws ClientException {
+        checkClosed();
+        return addConnection(new ClientConnection(this, host, -1, new ConnectionOptions(options)).connect());
+    }
+
+    @Override
+    public String containerId() {
+        return options.id();
+    }
+
+    String getClientUniqueId() {
+        return clientUniqueId;
+    }
+
+    ClientOptions options() {
+        return options;
+    }
+
+    @Override
+    public void close() {
+        try {
+            closeAsync().get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public synchronized Future<Client> closeAsync() {
+        if (!closed) {
+            closed = true;
+
+            if (connections.isEmpty()) {
+                closedFuture.complete(this);
+            } else {
+                List<Connection> connectionsView = new ArrayList<>(connections.values());
+                connectionsView.forEach((connection) -> connection.close());
+
+                for (Connection connection : connectionsView) {
+                    try {
+                        connection.close();
+                    } catch (Throwable ignored) {
+                        LOG.trace("Error while closing connection, ignoring", ignored);
+                    }
+                }
+            }
+        }
+
+        return closedFuture;
+    }
+
+    //----- Internal API
+
+    private void checkClosed() throws ClientIllegalStateException {
+        if (closed) {
+            throw new ClientIllegalStateException("Cannot create new connections, the Client has been closed.");
+        }
+    }
+
+    String nextConnectionId() {
+        return getClientUniqueId() + ":" + CONNECTION_COUNTER.incrementAndGet();
+    }
+
+    private ClientConnection addConnection(ClientConnection connection) {
+        connections.put(connection.getId(), connection);
+        return connection;
+    }
+
+    void unregisterConnection(ClientConnection connection) {
+        synchronized (connections) {
+            connections.remove(connection.getId());
+            if (closed && connections.isEmpty()) {
+                closedFuture.complete(this);
+            }
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientLocalTransactionContext.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientLocalTransactionContext.java
new file mode 100644
index 0000000..2661b65
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientLocalTransactionContext.java
@@ -0,0 +1,450 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionDeclarationException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionNotActiveException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionRolledBackException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.Transaction;
+import org.apache.qpid.protonj2.engine.Transaction.DischargeState;
+import org.apache.qpid.protonj2.engine.TransactionController;
+import org.apache.qpid.protonj2.engine.TransactionState;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transactions.TxnCapability;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Transaction context used to manage a running transaction within a single {@link Session}
+ */
+final class ClientLocalTransactionContext implements ClientTransactionContext {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientLocalTransactionContext.class);
+
+    private static final Symbol[] SUPPORTED_OUTCOMES = new Symbol[] { Accepted.DESCRIPTOR_SYMBOL,
+                                                                      Rejected.DESCRIPTOR_SYMBOL,
+                                                                      Released.DESCRIPTOR_SYMBOL,
+                                                                      Modified.DESCRIPTOR_SYMBOL };
+
+    private final String DECLARE_FUTURE_NAME = "Declare:Future";
+    private final String DISCHARGE_FUTURE_NAME = "Discharge:Future";
+    private final String START_TRANSACTION_MARKER = "Transaction:Start";
+
+    private final ClientSession session;
+
+    private Transaction<TransactionController> currentTxn;
+    private TransactionController txnController;
+
+    private TransactionalState cachedSenderOutcome;
+    private TransactionalState cachedReceiverOutcome;
+
+    public ClientLocalTransactionContext(ClientSession session) {
+        this.session = session;
+    }
+
+    @Override
+    public ClientLocalTransactionContext begin(ClientFuture<Session> beginFuture) throws ClientIllegalStateException {
+        checkCanBeginNewTransaction();
+        beginNewTransaction(beginFuture);
+        return this;
+    }
+
+    @Override
+    public ClientLocalTransactionContext commit(ClientFuture<Session> commitFuture, boolean startNew) throws ClientIllegalStateException {
+        checkCanCommitTransaction();
+
+        if (txnController.isLocallyOpen()) {
+            currentTxn.getAttachments().set(DISCHARGE_FUTURE_NAME, commitFuture);
+            currentTxn.getAttachments().set(START_TRANSACTION_MARKER, startNew);
+
+            if (session.options().requestTimeout() > 0) {
+                session.scheduleRequestTimeout(commitFuture, session.options().requestTimeout(), () -> {
+                    try {
+                        txnController.close();
+                    } catch (Exception ignore) {
+                    }
+
+                    return new ClientTransactionRolledBackException("Timed out waiting for Transaction commit to complete");
+                });
+            }
+
+            txnController.addCapacityAvailableHandler(controller -> {
+                try {
+                    txnController.discharge(currentTxn, false);
+                } catch (EngineFailedException efe) {
+                    commitFuture.failed(ClientExceptionSupport.createOrPassthroughFatal(efe));
+                }
+            });
+        } else {
+            currentTxn = null;
+            // The coordinator link closed which amount to a roll back of the declared
+            // transaction so we just complete the request as a failure.
+            commitFuture.failed(createRolledBackErrorFromClosedCoordinator());
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientLocalTransactionContext rollback(ClientFuture<Session> rollbackFuture, boolean startNew) throws ClientIllegalStateException {
+        checkCanRollbackTransaction();
+
+        if (txnController.isLocallyOpen()) {
+            currentTxn.getAttachments().set(DISCHARGE_FUTURE_NAME, rollbackFuture);
+            currentTxn.getAttachments().set(START_TRANSACTION_MARKER, startNew);
+
+            if (session.options().requestTimeout() > 0) {
+                session.scheduleRequestTimeout(rollbackFuture, session.options().requestTimeout(), () -> {
+                    try {
+                        txnController.close();
+                    } catch (Exception ignore) {
+                    }
+
+                    return new ClientOperationTimedOutException("Timed out waiting for Transaction rollback to complete");
+                });
+            }
+
+            txnController.addCapacityAvailableHandler(controller -> {
+                try {
+                    txnController.discharge(currentTxn, true);
+                } catch (EngineFailedException efe) {
+                    // The engine has failed and the connection will be closed so the transaction
+                    // is implicitly rolled back on the remote.
+                    rollbackFuture.complete(session);
+                } catch (Throwable efe) {
+                    // Some internal error has occurred and should be communicated as this is not
+                    // expected under normal circumstances.
+                    rollbackFuture.failed(ClientExceptionSupport.createOrPassthroughFatal(efe));
+                }
+            });
+        } else {
+            currentTxn = null;
+            // Coordinator was closed after transaction was declared which amounts
+            // to a roll back of the transaction so we let this complete as normal.
+            rollbackFuture.complete(session);
+        }
+
+        return this;
+    }
+
+    @Override
+    public boolean isInTransaction() {
+        return currentTxn != null && currentTxn.getState() == TransactionState.DECLARED;
+    }
+
+    @Override
+    public boolean isRollbackOnly() {
+        if (isInTransaction()) {
+            return txnController.isLocallyClosed();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public ClientTransactionContext send(ClientOutgoingEnvelope envelope, DeliveryState outcome, boolean settled) {
+        if (isInTransaction()) {
+            if (isRollbackOnly()) {
+                envelope.discard();
+            } else if (outcome == null) {
+                DeliveryState txnOutcome = cachedSenderOutcome != null ?
+                    cachedSenderOutcome : (cachedSenderOutcome = new TransactionalState().setTxnId(currentTxn.getTxnId()));
+                envelope.sendPayload(txnOutcome, settled);
+            } else {
+                envelope.sendPayload(new TransactionalState().setTxnId(currentTxn.getTxnId()).setOutcome((Outcome) outcome), settled);
+            }
+        } else {
+            envelope.sendPayload(outcome, settled);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientTransactionContext disposition(IncomingDelivery delivery, DeliveryState outcome, boolean settled) {
+        if (isInTransaction()) {
+            final DeliveryState txnOutcome;
+            if (outcome instanceof Accepted) {
+                txnOutcome = cachedReceiverOutcome != null ? cachedReceiverOutcome :
+                    (cachedReceiverOutcome = new TransactionalState().setTxnId(currentTxn.getTxnId()).setOutcome(Accepted.getInstance()));
+            } else {
+                txnOutcome = new TransactionalState().setTxnId(currentTxn.getTxnId()).setOutcome((Outcome) outcome);
+            }
+
+            delivery.disposition(txnOutcome, true);
+        } else {
+            delivery.disposition(outcome, settled);
+        }
+
+        return this;
+    }
+
+    //------ Internals of Transaction State management
+
+    private void beginNewTransaction(ClientFuture<Session> beginFuture) {
+        TransactionController txnController = getOrCreateNewTxnController();
+
+        currentTxn = txnController.newTransaction();
+        currentTxn.setLinkedResource(this);
+        currentTxn.getAttachments().set(DECLARE_FUTURE_NAME, beginFuture);
+
+        cachedReceiverOutcome = null;
+        cachedSenderOutcome = null;
+
+        if (session.options().requestTimeout() > 0) {
+            session.scheduleRequestTimeout(beginFuture, session.options().requestTimeout(), () -> {
+                try {
+                    txnController.close();
+                } catch (Exception ignore) {
+                }
+
+                return new ClientTransactionDeclarationException("Timed out waiting for Transaction declaration to complete");
+            });
+        }
+
+        txnController.addCapacityAvailableHandler(controller -> {
+            try {
+                txnController.declare(currentTxn);
+            } catch (EngineFailedException efe) {
+                beginFuture.failed(ClientExceptionSupport.createOrPassthroughFatal(efe));
+            }
+        });
+    }
+
+    private TransactionController getOrCreateNewTxnController() {
+        if (txnController == null || txnController.isLocallyClosed()) {
+            Coordinator coordinator = new Coordinator();
+            coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+
+            Source source = new Source();
+            source.setOutcomes(Arrays.copyOf(SUPPORTED_OUTCOMES, SUPPORTED_OUTCOMES.length));
+
+            TransactionController txnController = session.getProtonSession().coordinator("Coordinator:" + session.id());
+            txnController.setSource(source)
+                         .setCoordinator(coordinator)
+                         .declaredHandler(this::handleTransactionDeclared)
+                         .declareFailureHandler(this::handleTransactionDeclareFailed)
+                         .dischargedHandler(this::handleTransactionDischarged)
+                         .dischargeFailureHandler(this::handleTransactionDischargeFailed)
+                         .openHandler(this::handleCoordinatorOpen)
+                         .closeHandler(this::handleCoordinatorClose)
+                         .localCloseHandler(this::handleCoordinatorLocalClose)
+                         .parentEndpointClosedHandler(this::handleParentEndpointClosed)
+                         .engineShutdownHandler(this::handleEngineShutdown)
+                         .open();
+
+            this.txnController = txnController;
+        }
+
+        return txnController;
+    }
+
+    private void checkCanBeginNewTransaction() throws ClientIllegalStateException {
+        if (currentTxn != null) {
+            switch (currentTxn.getState()) {
+                case DISCHARGED:
+                case DISCHARGE_FAILED:
+                case DECLARE_FAILED:
+                    break;
+                case DECLARING:
+                    throw new ClientIllegalStateException("A transaction is already in the process of being started");
+                case DECLARED:
+                    throw new ClientIllegalStateException("A transaction is already active in this Session");
+                case DISCHARGING:
+                    throw new ClientIllegalStateException("A transaction is still being retired and a new one cannot yet be started");
+                default:
+                    throw new ClientIllegalStateException("Cannot begin a new transaction until the existing transaction completes");
+            }
+        }
+    }
+
+    private void checkCanCommitTransaction() throws ClientIllegalStateException {
+        if (currentTxn == null) {
+            throw new ClientTransactionNotActiveException("Commit called with no active transaction");
+        } else {
+            switch (currentTxn.getState()) {
+                case DISCHARGED:
+                    throw new ClientTransactionNotActiveException("Commit called with no active transaction");
+                case DECLARING:
+                    throw new ClientIllegalStateException("Commit called before transaction declare completed.");
+                case DISCHARGING:
+                    throw new ClientIllegalStateException("Commit called before transaction discharge completed.");
+                case DECLARE_FAILED:
+                    throw new ClientTransactionNotActiveException("Commit called on a transaction that has failed due to an error during declare.");
+                case DISCHARGE_FAILED:
+                    throw new ClientTransactionNotActiveException("Commit called on a transaction that has failed due to an error during discharge.");
+                case IDLE:
+                    throw new ClientTransactionNotActiveException("Commit called on a transaction that has not yet been declared");
+                default:
+                    break;
+            }
+        }
+    }
+
+    private void checkCanRollbackTransaction() throws ClientIllegalStateException {
+        if (currentTxn == null) {
+            throw new ClientTransactionNotActiveException("Rollback called with no active transaction");
+        } else {
+            switch (currentTxn.getState()) {
+                case DISCHARGED:
+                    throw new ClientTransactionNotActiveException("Rollback called with no active transaction");
+                case DECLARING:
+                    throw new ClientIllegalStateException("Rollback called before transaction declare completed.");
+                case DISCHARGING:
+                    throw new ClientIllegalStateException("Rollback called before transaction discharge completed.");
+                case DECLARE_FAILED:
+                    throw new ClientTransactionNotActiveException("Rollback called on a transaction that has failed due to an error during declare.");
+                case DISCHARGE_FAILED:
+                    throw new ClientTransactionNotActiveException("Rollback called on a transaction that has failed due to an error during discharge.");
+                case IDLE:
+                    throw new ClientTransactionNotActiveException("Rollback called on a transaction that has not yet been declared");
+                default:
+                    break;
+            }
+        }
+    }
+
+    //----- Handle events from the Transaction Controller
+
+    private void handleTransactionDeclared(Transaction<TransactionController> transaction) {
+        ClientFuture<Session> future = transaction.getAttachments().get(DECLARE_FUTURE_NAME);
+        LOG.trace("Declare of trasaction:{} completed", transaction);
+
+        if (future.isComplete() || future.isCancelled()) {
+            // The original declare operation cancelled the future likely due to timeout
+            // which means this transaction will never be completed at a higher level so we
+            // must discharge it now to ensure the remote can clean up associated resources.
+            try {
+                rollback(session.getFutureFactory().createFuture(), false);
+            } catch (Exception ignore) {}
+        } else {
+            future.complete(session);
+        }
+    }
+
+    private void handleTransactionDeclareFailed(Transaction<TransactionController> transaction) {
+        ClientFuture<Session> future = transaction.getAttachments().get(DECLARE_FUTURE_NAME);
+        LOG.trace("Declare of trasaction:{} failed", transaction);
+        ClientException cause = ClientExceptionSupport.convertToNonFatalException(transaction.getCondition());
+        future.failed(new ClientTransactionDeclarationException(cause.getMessage(), cause));
+    }
+
+    private void handleTransactionDischarged(Transaction<TransactionController> transaction) {
+        ClientFuture<Session> future = transaction.getAttachments().get(DISCHARGE_FUTURE_NAME);
+        LOG.trace("Discharge of trasaction:{} completed", transaction);
+        future.complete(session);
+
+        if (Boolean.TRUE.equals(transaction.getAttachments().get(START_TRANSACTION_MARKER))) {
+            beginNewTransaction(future);
+        }
+    }
+
+    private void handleTransactionDischargeFailed(Transaction<TransactionController> transaction) {
+        ClientFuture<Session> future = transaction.getAttachments().get(DISCHARGE_FUTURE_NAME);
+        LOG.trace("Discharge of trasaction:{} failed", transaction);
+        ClientException cause = ClientExceptionSupport.convertToNonFatalException(transaction.getCondition());
+        future.failed(new ClientTransactionRolledBackException(cause.getMessage(), cause));
+    }
+
+    private void handleCoordinatorOpen(TransactionController controller) {
+        // If remote doesn't set a remote Coordinator then a close is incoming.
+        if (controller.getRemoteCoordinator() != null) {
+            this.txnController = controller;
+        }
+    }
+
+    private void handleCoordinatorClose(TransactionController controller) {
+        if (txnController != null) {
+            txnController.close();
+        }
+    }
+
+    private ClientTransactionRolledBackException createRolledBackErrorFromClosedCoordinator() {
+        ClientException cause = ClientExceptionSupport.convertToNonFatalException(txnController.getRemoteCondition());
+
+        if (!(cause instanceof ClientTransactionRolledBackException)) {
+            cause = new ClientTransactionRolledBackException(cause.getMessage(), cause);
+        }
+
+        return (ClientTransactionRolledBackException) cause;
+    }
+
+    private ClientTransactionDeclarationException createDeclarationErrorFromClosedCoordinator() {
+        ClientException cause = ClientExceptionSupport.convertToNonFatalException(txnController.getRemoteCondition());
+
+        if (!(cause instanceof ClientTransactionDeclarationException)) {
+            cause = new ClientTransactionDeclarationException(cause.getMessage(), cause);
+        }
+
+        return (ClientTransactionDeclarationException) cause;
+    }
+
+    private void handleCoordinatorLocalClose(TransactionController controller) {
+        if (currentTxn != null) {
+            ClientFuture<Session> future = null;
+
+            switch (currentTxn.getState()) {
+                case IDLE:
+                case DECLARING:
+                    future = currentTxn.getAttachments().get(DECLARE_FUTURE_NAME);
+                    future.failed(createDeclarationErrorFromClosedCoordinator());
+                    currentTxn = null;
+                    break;
+                case DISCHARGING:
+                    future = currentTxn.getAttachments().get(DISCHARGE_FUTURE_NAME);
+                    if (currentTxn.getDischargeState() == DischargeState.COMMIT) {
+                        future.failed(createRolledBackErrorFromClosedCoordinator());
+                    } else {
+                        future.complete(session);
+                    }
+                    currentTxn = null;
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private void handleParentEndpointClosed(TransactionController txnController) {
+        txnController.close();
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        if (txnController != null) {
+            txnController.close();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientMessage.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientMessage.java
new file mode 100644
index 0000000..c3dafcb
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientMessage.java
@@ -0,0 +1,728 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+
+public class ClientMessage<E> implements AdvancedMessage<E> {
+
+    private Header header;
+    private MessageAnnotations messageAnnotations;
+    private Properties properties;
+    private ApplicationProperties applicationProperties;
+    private Section<E> body;
+    private List<Section<?>> bodySections;
+    private Footer footer;
+
+    private int messageFormat;
+
+    /**
+     * Create a new {@link ClientMessage} instance with no default body section or
+     * section supplier
+     *
+     * @param sectionSupplier
+     *      A {@link Supplier} that will generate Section values for the message body.
+     */
+    ClientMessage() {
+        this.body = null;
+    }
+
+    /**
+     * Create a new {@link ClientMessage} instance with a {@link Supplier} that will
+     * provide the AMQP {@link Section} value for any body that is set on the message.
+     *
+     * @param body
+     *      The object that comprises the value portion of the body {@link Section}.
+     */
+    ClientMessage(Section<E> body) {
+        this.body = body;
+    }
+
+    @Override
+    public AdvancedMessage<E> toAdvancedMessage() {
+        return this;
+    }
+
+    //----- Entry point for creating new ClientMessage instances.
+
+    /**
+     * Creates an empty {@link ClientMessage} instance.
+     *
+     * @param <V> The type of the body value carried in this message.
+     *
+     * @return a new empty {@link ClientMessage} instance.
+     */
+    public static <V> ClientMessage<V> create() {
+        return new ClientMessage<V>();
+    }
+
+    /**
+     * Creates an {@link ClientMessage} instance with the given body {@link Section} value.
+     *
+     * @param <V> The type of the body value carried in this message body section.
+     *
+     * @param body
+     *      The body {@link Section} to assign to the created message isntance.
+     *
+     * @return a new {@link ClientMessage} instance with the given body.
+     */
+    public static <V> ClientMessage<V> create(Section<V> body) {
+        return new ClientMessage<V>(body);
+    }
+
+    /**
+     * Creates an empty {@link ClientMessage} instance.
+     *
+     * @param <V> The type of the body value carried in this message.
+     *
+     * @return a new empty {@link ClientMessage} instance.
+     */
+    public static <V> ClientMessage<V> createAdvancedMessage() {
+        return new ClientMessage<V>();
+    }
+
+    //----- Message Header API
+
+    @Override
+    public boolean durable() {
+        return header == null ? Header.DEFAULT_DURABILITY : header.isDurable();
+    }
+
+    @Override
+    public ClientMessage<E> durable(boolean durable) {
+        lazyCreateHeader().setDurable(durable);
+        return this;
+    }
+
+    @Override
+    public byte priority() {
+        return header == null ? Header.DEFAULT_PRIORITY : header.getPriority();
+    }
+
+    @Override
+    public ClientMessage<E> priority(byte priority) {
+        lazyCreateHeader().setPriority(priority);
+        return this;
+    }
+
+    @Override
+    public long timeToLive() {
+        return header == null ? Header.DEFAULT_TIME_TO_LIVE : header.getTimeToLive();
+    }
+
+    @Override
+    public ClientMessage<E> timeToLive(long timeToLive) {
+        lazyCreateHeader().setTimeToLive(timeToLive);
+        return this;
+    }
+
+    @Override
+    public boolean firstAcquirer() {
+        return header == null ? Header.DEFAULT_FIRST_ACQUIRER : header.isFirstAcquirer();
+    }
+
+    @Override
+    public ClientMessage<E> firstAcquirer(boolean firstAcquirer) {
+        lazyCreateHeader().setFirstAcquirer(firstAcquirer);
+        return this;
+    }
+
+    @Override
+    public long deliveryCount() {
+        return header == null ? Header.DEFAULT_DELIVERY_COUNT : header.getDeliveryCount();
+    }
+
+    @Override
+    public ClientMessage<E> deliveryCount(long deliveryCount) {
+        lazyCreateHeader().setDeliveryCount(deliveryCount);
+        return this;
+    }
+
+    //----- Message Properties access
+
+    @Override
+    public Object messageId() {
+        return properties != null ? properties.getMessageId() : null;
+    }
+
+    @Override
+    public Message<E> messageId(Object messageId) {
+        lazyCreateProperties().setMessageId(messageId);
+        return this;
+    }
+
+    @Override
+    public byte[] userId() {
+        byte[] copyOfUserId = null;
+        if (properties != null && properties.getUserId() != null) {
+            copyOfUserId = properties.getUserId().arrayCopy();
+        }
+
+        return copyOfUserId;
+    }
+
+    @Override
+    public Message<E> userId(byte[] userId) {
+        lazyCreateProperties().setUserId(new Binary(Arrays.copyOf(userId, userId.length)));
+        return this;
+    }
+
+    @Override
+    public String to() {
+        return properties != null ? properties.getTo() : null;
+    }
+
+    @Override
+    public Message<E> to(String to) {
+        lazyCreateProperties().setTo(to);
+        return this;
+    }
+
+    @Override
+    public String subject() {
+        return properties != null ? properties.getSubject() : null;
+    }
+
+    @Override
+    public Message<E> subject(String subject) {
+        lazyCreateProperties().setSubject(subject);
+        return this;
+    }
+
+    @Override
+    public String replyTo() {
+        return properties != null ? properties.getReplyTo() : null;
+    }
+
+    @Override
+    public Message<E> replyTo(String replyTo) {
+        lazyCreateProperties().setReplyTo(replyTo);
+        return this;
+    }
+
+    @Override
+    public Object correlationId() {
+        return properties != null ? properties.getCorrelationId() : null;
+    }
+
+    @Override
+    public Message<E> correlationId(Object correlationId) {
+        lazyCreateProperties().setCorrelationId(correlationId);
+        return this;
+    }
+
+    @Override
+    public String contentType() {
+        return properties != null ? properties.getContentType() : null;
+    }
+
+    @Override
+    public Message<E> contentType(String contentType) {
+        lazyCreateProperties().setContentType(contentType);
+        return this;
+    }
+
+    @Override
+    public String contentEncoding() {
+        return properties != null ? properties.getContentEncoding() : null;
+    }
+
+    @Override
+    public Message<E> contentEncoding(String contentEncoding) {
+        lazyCreateProperties().setContentEncoding(contentEncoding);
+        return this;
+    }
+
+    @Override
+    public long absoluteExpiryTime() {
+        return properties != null ? properties.getAbsoluteExpiryTime() : 0;
+    }
+
+    @Override
+    public Message<E> absoluteExpiryTime(long expiryTime) {
+        lazyCreateProperties().setAbsoluteExpiryTime(expiryTime);
+        return this;
+    }
+
+    @Override
+    public long creationTime() {
+        return properties != null ? properties.getCreationTime() : 0;
+    }
+
+    @Override
+    public Message<E> creationTime(long createTime) {
+        lazyCreateProperties().setCreationTime(createTime);
+        return this;
+    }
+
+    @Override
+    public String groupId() {
+        return properties != null ? properties.getGroupId() : null;
+    }
+
+    @Override
+    public Message<E> groupId(String groupId) {
+        lazyCreateProperties().setGroupId(groupId);
+        return this;
+    }
+
+    @Override
+    public int groupSequence() {
+        return properties != null ? (int) properties.getGroupSequence() : 0;
+    }
+
+    @Override
+    public Message<E> groupSequence(int groupSequence) {
+        lazyCreateProperties().setGroupSequence(groupSequence);
+        return this;
+    }
+
+    @Override
+    public String replyToGroupId() {
+        return properties != null ? properties.getReplyToGroupId() : null;
+    }
+
+    @Override
+    public Message<E> replyToGroupId(String replyToGroupId) {
+        lazyCreateProperties().setReplyToGroupId(replyToGroupId);
+        return this;
+    }
+
+    //----- Message Annotations Access
+
+    @Override
+    public Object annotation(String key) {
+        if (hasAnnotations()) {
+            return messageAnnotations.getValue().get(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean hasAnnotation(String key) {
+        if (hasAnnotations()) {
+            return messageAnnotations.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasAnnotations() {
+        return messageAnnotations != null &&
+               messageAnnotations.getValue() != null &&
+               messageAnnotations.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeAnnotation(String key) {
+        if (hasAnnotations()) {
+            return messageAnnotations.getValue().remove(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public Message<E> forEachAnnotation(BiConsumer<String, Object> action) {
+        if (hasAnnotations()) {
+            messageAnnotations.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientMessage<E> annotation(String key, Object value) {
+        lazyCreateMessageAnnotations().getValue().put(Symbol.valueOf(key),value);
+        return this;
+    }
+
+    //----- Application Properties Access
+
+    @Override
+    public Object property(String key) {
+        if (hasProperties()) {
+            return applicationProperties.getValue().get(key);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean hasProperty(String key) {
+        if (hasProperties()) {
+            return applicationProperties.getValue().containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasProperties() {
+        return applicationProperties != null &&
+               applicationProperties.getValue() != null &&
+               applicationProperties.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeProperty(String key) {
+        if (hasProperties()) {
+            return applicationProperties.getValue().remove(key);
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public Message<E> forEachProperty(BiConsumer<String, Object> action) {
+        if (hasProperties()) {
+            applicationProperties.getValue().forEach(action);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientMessage<E> property(String key, Object value) {
+        lazyCreateApplicationProperties().getValue().put(key,value);
+        return this;
+    }
+
+    //----- Footer Access
+
+    @Override
+    public Object footer(String key) {
+        if (hasFooters()) {
+            return footer.getValue().get(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean hasFooter(String key) {
+        if (hasFooters()) {
+            return footer.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasFooters() {
+        return footer != null &&
+               footer.getValue() != null &&
+               footer.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeFooter(String key) {
+        if (hasFooters()) {
+            return footer.getValue().remove(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public Message<E> forEachFooter(BiConsumer<String, Object> action) {
+        if (hasFooters()) {
+            footer.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientMessage<E> footer(String key, Object value) {
+        lazyCreateFooter().getValue().put(Symbol.valueOf(key),value);
+        return this;
+    }
+
+    //----- Message body access
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public E body() {
+        Section<E> section = body;
+
+        if (bodySections != null) {
+            section = (Section<E>) bodySections.get(0);
+        }
+
+        return section != null ? section.getValue() : null;
+    }
+
+    @Override
+    public ClientMessage<E> body(E value) {
+        if (bodySections != null) {
+            if (value != null) {
+                bodySections.set(0, ClientMessageSupport.createSectionFromValue(value));
+            } else {
+                bodySections = null;
+            }
+        } else {
+            body = ClientMessageSupport.createSectionFromValue(value);
+        }
+
+        return this;
+    }
+
+    //----- Internal API
+
+    private Header lazyCreateHeader() {
+        if (header == null) {
+            header = new Header();
+        }
+
+        return header;
+    }
+
+    private Properties lazyCreateProperties() {
+        if (properties == null) {
+            properties = new Properties();
+        }
+
+        return properties;
+    }
+
+    private ApplicationProperties lazyCreateApplicationProperties() {
+        if (applicationProperties == null) {
+            applicationProperties = new ApplicationProperties(new LinkedHashMap<>());
+        }
+
+        return applicationProperties;
+    }
+
+    private MessageAnnotations lazyCreateMessageAnnotations() {
+        if (messageAnnotations == null) {
+            messageAnnotations = new MessageAnnotations(new LinkedHashMap<>());
+        }
+
+        return messageAnnotations;
+    }
+
+    private Footer lazyCreateFooter() {
+        if (footer == null) {
+            footer = new Footer(new LinkedHashMap<>());
+        }
+
+        return footer;
+    }
+
+    //----- AdvancedMessage interface implementation
+
+    @Override
+    public Header header() {
+        return header;
+    }
+
+    @Override
+    public ClientMessage<E> header(Header header) {
+        this.header = header;
+        return this;
+    }
+
+    @Override
+    public MessageAnnotations annotations() {
+        return messageAnnotations;
+    }
+
+    @Override
+    public ClientMessage<E> annotations(MessageAnnotations messageAnnotations) {
+        this.messageAnnotations = messageAnnotations;
+        return this;
+    }
+
+    @Override
+    public Properties properties() {
+        return properties;
+    }
+
+    @Override
+    public ClientMessage<E> properties(Properties properties) {
+        this.properties = properties;
+        return this;
+    }
+
+    @Override
+    public ApplicationProperties applicationProperties() {
+        return applicationProperties;
+    }
+
+    @Override
+    public ClientMessage<E> applicationProperties(ApplicationProperties applicationProperties) {
+        this.applicationProperties = applicationProperties;
+        return this;
+    }
+
+    @Override
+    public Footer footer() {
+        return footer;
+    }
+
+    @Override
+    public ClientMessage<E> footer(Footer footer) {
+        this.footer = footer;
+        return this;
+    }
+
+    @Override
+    public int messageFormat() {
+        return messageFormat;
+    }
+
+    @Override
+    public ClientMessage<E> messageFormat(int messageFormat) {
+        this.messageFormat = messageFormat;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer encode(Map<String, Object> deliveryAnnotations) throws ClientException {
+        return ClientMessageSupport.encodeMessage(this, deliveryAnnotations);
+    }
+
+    @SuppressWarnings({ "unchecked" })
+    @Override
+    public ClientMessage<E> addBodySection(Section<?> bodySection) {
+        Objects.requireNonNull(bodySection, "Additional Body Section cannot be null");
+
+        if (body == null && bodySections == null) {
+            body = (Section<E>) bodySection;
+        } else {
+            if (bodySections == null) {
+                bodySections = new ArrayList<>();
+
+                // Preserve older section from original message creation.
+                if (body != null) {
+                    bodySections.add(body);
+                    body = null;
+                }
+            }
+
+            bodySections.add(validateBodySections(messageFormat, bodySections, bodySection));
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientMessage<E> bodySections(Collection<Section<?>> sections) {
+        if (sections == null || sections.isEmpty()) {
+            bodySections = null;
+        } else {
+            List<Section<?>> result = new ArrayList<>(sections.size());
+            sections.forEach(section -> result.add(validateBodySections(messageFormat, result, section)));
+            bodySections = result;
+        }
+
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<Section<?>> bodySections() {
+        if (bodySections == null && body == null) {
+            return Collections.EMPTY_LIST;
+        } else {
+            final Collection<Section<?>> result = new ArrayList<>();
+            forEachBodySection(section -> result.add(section));
+            return Collections.unmodifiableCollection(result);
+        }
+    }
+
+    @Override
+    public ClientMessage<E> forEachBodySection(Consumer<Section<?>> consumer) {
+        if (bodySections != null) {
+            bodySections.forEach(section -> {
+                consumer.accept(section);
+            });
+        } else {
+            if (body != null) {
+                consumer.accept(body);
+            }
+        }
+
+        return this;
+    }
+
+
+    @Override
+    public ClientMessage<E> clearBodySections() {
+        bodySections = null;
+        body = null;
+
+        return this;
+    }
+
+    private static Section<?> validateBodySections(int messageFormat, List<Section<?>> target, Section<?> section) {
+        if (messageFormat == 0 && target != null && !target.isEmpty()) {
+            switch (section.getType()) {
+                case AmqpSequence:
+                    if (target.get(0).getType() != SectionType.AmqpSequence) {
+                        throw new IllegalArgumentException(
+                            "Message Format violation: AmqpSequence expected but got type: " + section.getType());
+                    }
+                    break;
+                case AmqpValue:
+                    throw new IllegalArgumentException(
+                        "Message Format violation: Only one AmqpValue section allowed");
+                case Data:
+                    if (target.get(0).getType() != SectionType.Data) {
+                        throw new IllegalArgumentException(
+                            "Message Format violation: Data Section expected but got type: " + section.getType());
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return section;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientMessageSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientMessageSupport.java
new file mode 100644
index 0000000..a7cba17
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientMessageSupport.java
@@ -0,0 +1,275 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+
+/**
+ * Support methods dealing with Message types and encode or decode operations.
+ */
+public abstract class ClientMessageSupport {
+
+    private static final Encoder DEFAULT_ENCODER = CodecFactory.getDefaultEncoder();
+    private static final Decoder DEFAULT_DECODER = CodecFactory.getDefaultDecoder();
+
+    //----- Message Conversion
+
+    /**
+     * Converts a {@link Message} instance into a {@link ClientMessage} instance
+     * either by cast or by construction of a new instance with a copy of the
+     * values carried in the given message.
+     *
+     * @param <E> the body type of the given message.
+     *
+     * @param message
+     *      The {@link Message} type to attempt to convert to a {@link ClientMessage} instance.
+     *
+     * @return a {@link ClientMessage} that represents the given {@link Message} instance.
+     *
+     * @throws ClientException if an unrecoverable error occurs during message conversion.
+     */
+    public static <E> AdvancedMessage<E> convertMessage(Message<E> message) throws ClientException {
+        if (message instanceof AdvancedMessage) {
+            return (AdvancedMessage<E>) message;
+        } else {
+            try {
+                return message.toAdvancedMessage();
+            } catch (UnsupportedOperationException uoe) {
+                return convertFromOutsideMessage(message);
+            }
+        }
+    }
+
+    //----- Message Encoding
+
+    public static ProtonBuffer encodeSection(Section<?> section, ProtonBuffer buffer) {
+        DEFAULT_ENCODER.writeObject(buffer, DEFAULT_ENCODER.newEncoderState(), section);
+        return buffer;
+    }
+
+    //----- Message Encoding
+
+    public static ProtonBuffer encodeMessage(AdvancedMessage<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        return encodeMessage(DEFAULT_ENCODER, DEFAULT_ENCODER.newEncoderState(), ProtonByteBufferAllocator.DEFAULT, message, deliveryAnnotations);
+    }
+
+    public static ProtonBuffer encodeMessage(Encoder encoder, ProtonBufferAllocator allocator, AdvancedMessage<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        return encodeMessage(encoder, encoder.newEncoderState(), ProtonByteBufferAllocator.DEFAULT, message, deliveryAnnotations);
+    }
+
+    public static ProtonBuffer encodeMessage(Encoder encoder, EncoderState encoderState, ProtonBufferAllocator allocator, AdvancedMessage<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        ProtonBuffer buffer = allocator.allocate();
+
+        Header header = message.header();
+        MessageAnnotations messageAnnotations = message.annotations();
+        Properties properties = message.properties();
+        ApplicationProperties applicationProperties = message.applicationProperties();
+        Footer footer = message.footer();
+
+        if (header != null) {
+            encoder.writeObject(buffer, encoderState, header);
+        }
+        if (deliveryAnnotations != null) {
+            encoder.writeObject(buffer, encoderState, new DeliveryAnnotations(StringUtils.toSymbolKeyedMap(deliveryAnnotations)));
+        }
+        if (messageAnnotations != null) {
+            encoder.writeObject(buffer, encoderState, messageAnnotations);
+        }
+        if (properties != null) {
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+        if (applicationProperties != null) {
+            encoder.writeObject(buffer, encoderState, applicationProperties);
+        }
+
+        message.forEachBodySection(section -> encoder.writeObject(buffer, encoderState, section));
+
+        if (footer != null) {
+            encoder.writeObject(buffer, encoderState, footer);
+        }
+
+        return buffer;
+    }
+
+    //----- Message Decoding
+
+    public static Message<?> decodeMessage(ProtonBuffer buffer, Consumer<DeliveryAnnotations> daConsumer) throws ClientException {
+        return decodeMessage(DEFAULT_DECODER, DEFAULT_DECODER.newDecoderState(), buffer, daConsumer);
+    }
+
+    public static Message<?> decodeMessage(Decoder decoder, ProtonBuffer buffer, Consumer<DeliveryAnnotations> daConsumer) throws ClientException {
+        return decodeMessage(decoder, decoder.newDecoderState(), buffer, daConsumer);
+    }
+
+    public static Message<?> decodeMessage(Decoder decoder, DecoderState decoderState,
+                                           ProtonBuffer buffer, Consumer<DeliveryAnnotations> daConsumer) throws ClientException {
+
+        final ClientMessage<?> message = new ClientMessage<>();
+
+        Section<?> section = null;
+
+        while (buffer.isReadable()) {
+            try {
+                section = (Section<?>) decoder.readObject(buffer, decoderState);
+            } catch (Exception e) {
+                throw ClientExceptionSupport.createNonFatalOrPassthrough(e);
+            }
+
+            switch (section.getType()) {
+                case Header:
+                    message.header((Header) section);
+                    break;
+                case DeliveryAnnotations:
+                    if (daConsumer != null) {
+                        daConsumer.accept((DeliveryAnnotations) section);
+                    }
+                    break;
+                case MessageAnnotations:
+                    message.annotations((MessageAnnotations) section);
+                    break;
+                case Properties:
+                    message.properties((Properties) section);
+                    break;
+                case ApplicationProperties:
+                    message.applicationProperties((ApplicationProperties) section);
+                    break;
+                case Data:
+                case AmqpSequence:
+                case AmqpValue:
+                    message.addBodySection(section);
+                    break;
+                case Footer:
+                    message.footer((Footer) section);
+                    break;
+                default:
+                    throw new ClientException("Unknown Message Section forced decode abort.");
+            }
+        }
+
+        return message;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static <E> Section<E> createSectionFromValue(E body) {
+        if (body == null) {
+            return null;
+        } else if (body instanceof byte[]) {
+            return (Section<E>) new Data((byte[]) body);
+        } else if (body instanceof List){
+            return new AmqpSequence((List) body);
+        } else {
+            return new AmqpValue(body);
+        }
+    }
+
+    //----- Internal Implementation
+
+    private static <E> ClientMessage<E> convertFromOutsideMessage(Message<E> source) throws ClientException {
+        Header header = new Header();
+        header.setDurable(source.durable());
+        header.setPriority(source.priority());
+        header.setTimeToLive(source.timeToLive());
+        header.setFirstAcquirer(source.firstAcquirer());
+        header.setDeliveryCount(source.deliveryCount());
+
+        Properties properties = new Properties();
+        properties.setMessageId(source.messageId());
+        properties.setUserId(source.userId() != null ? new Binary(source.userId()) : null);
+        properties.setTo(source.to());
+        properties.setSubject(source.subject());
+        properties.setReplyTo(source.replyTo());
+        properties.setCorrelationId(source.correlationId());
+        properties.setContentType(source.contentType());
+        properties.setContentEncoding(source.contentEncoding());
+        properties.setAbsoluteExpiryTime(source.absoluteExpiryTime());
+        properties.setCreationTime(source.creationTime());
+        properties.setGroupId(source.groupId());
+        properties.setGroupSequence(source.groupSequence());
+        properties.setReplyToGroupId(source.replyToGroupId());
+
+        final MessageAnnotations messageAnnotations;
+        if (source.hasAnnotations()) {
+            messageAnnotations = new MessageAnnotations(new LinkedHashMap<>());
+
+            source.forEachAnnotation((key, value) -> {
+                messageAnnotations.getValue().put(Symbol.valueOf(key), value);
+            });
+        } else {
+            messageAnnotations = null;
+        }
+
+        final ApplicationProperties applicationProperties;
+        if (source.hasProperties()) {
+            applicationProperties = new ApplicationProperties(new LinkedHashMap<>());
+
+            source.forEachProperty((key, value) -> {
+                applicationProperties.getValue().put(key, value);
+            });
+        } else {
+            applicationProperties = null;
+        }
+
+        final Footer footer;
+        if (source.hasFooters()) {
+            footer = new Footer(new LinkedHashMap<>());
+
+            source.forEachFooter((key, value) -> {
+                footer.getValue().put(Symbol.valueOf(key), value);
+            });
+        } else {
+            footer = null;
+        }
+
+        ClientMessage<E> message = new ClientMessage<>(createSectionFromValue(source.body()));
+
+        message.header(header);
+        message.properties(properties);
+        message.annotations(messageAnnotations);
+        message.applicationProperties(applicationProperties);
+        message.footer(footer);
+
+        return message;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpStreamTracker.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpStreamTracker.java
new file mode 100644
index 0000000..29975ca
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpStreamTracker.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamTracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+
+/**
+ * A dummy Tracker instance that always indicates remote settlement and
+ * acceptance for {@link StreamSender} instances.
+ */
+public class ClientNoOpStreamTracker extends ClientNoOpTracker implements StreamTracker {
+
+    public ClientNoOpStreamTracker(ClientStreamSender sender) {
+        super(sender);
+    }
+
+    @Override
+    public StreamSender sender() {
+        return (StreamSender) super.sender();
+    }
+
+    @Override
+    public StreamTracker settle() throws ClientException {
+        return (StreamTracker) super.settle();
+    }
+
+    @Override
+    public StreamTracker disposition(DeliveryState state, boolean settle) throws ClientException {
+        return (StreamTracker) super.disposition(state, settle);
+    }
+
+    @Override
+    public StreamTracker awaitSettlement() throws ClientException {
+        return this;
+    }
+
+    @Override
+    public StreamTracker awaitSettlement(long timeout, TimeUnit unit) throws ClientException {
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpTracker.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpTracker.java
new file mode 100644
index 0000000..b35060e
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpTracker.java
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.futures.ClientFutureFactory;
+
+/**
+ * A dummy Tracker instance that always indicates remote settlement and
+ * acceptance.
+ */
+public class ClientNoOpTracker implements Tracker {
+
+    private final ClientSender sender;
+
+    private DeliveryState state;
+    private boolean settled;
+
+    public ClientNoOpTracker(ClientSender sender) {
+        this.sender = sender;
+    }
+
+    @Override
+    public Sender sender() {
+        return sender;
+    }
+
+    @Override
+    public Tracker settle() throws ClientException {
+        this.settled = true;
+        return this;
+    }
+
+    @Override
+    public boolean settled() {
+        return settled;
+    }
+
+    @Override
+    public DeliveryState state() {
+        return state;
+    }
+
+    @Override
+    public Tracker disposition(DeliveryState state, boolean settle) throws ClientException {
+        this.state = state;
+        this.settled = settle;
+
+        return this;
+    }
+
+    @Override
+    public DeliveryState remoteState() {
+        return ClientDeliveryState.ClientAccepted.getInstance();
+    }
+
+    @Override
+    public boolean remoteSettled() {
+        return true;
+    }
+
+    @Override
+    public Future<Tracker> settlementFuture() {
+        return ClientFutureFactory.completedFuture(this);
+    }
+
+    @Override
+    public Tracker awaitSettlement() throws ClientException {
+        return this;
+    }
+
+    @Override
+    public Tracker awaitSettlement(long timeout, TimeUnit unit) throws ClientException {
+        return this;
+    }
+
+    @Override
+    public Tracker awaitAccepted() throws ClientException {
+        return this;
+    }
+
+    @Override
+    public Tracker awaitAccepted(long timeout, TimeUnit unit) throws ClientException {
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpTransactionContext.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpTransactionContext.java
new file mode 100644
index 0000000..ca4aead
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientNoOpTransactionContext.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+/**
+ * A pass-through {@link ClientTransactionContext} that is used when a session has not had any active
+ * transactions.
+ */
+final class ClientNoOpTransactionContext implements ClientTransactionContext {
+
+    @Override
+    public ClientTransactionContext begin(ClientFuture<Session> beginFuture) throws ClientIllegalStateException {
+        throw new ClientIllegalStateException("Cannot begin from a no-op transaction context");
+    }
+
+    @Override
+    public ClientTransactionContext commit(ClientFuture<Session> commitFuture, boolean startNew) throws ClientIllegalStateException {
+        throw new ClientIllegalStateException("Cannot commit from a no-op transaction context");
+    }
+
+    @Override
+    public ClientTransactionContext rollback(ClientFuture<Session> rollbackFuture, boolean startNew) throws ClientIllegalStateException {
+        throw new ClientIllegalStateException("Cannot rollback from a no-op transaction context");
+    }
+
+    @Override
+    public boolean isInTransaction() {
+        return false;
+    }
+
+    @Override
+    public boolean isRollbackOnly() {
+        return false;
+    }
+
+    @Override
+    public ClientTransactionContext send(ClientOutgoingEnvelope envelope, DeliveryState outcome, boolean settled) {
+        envelope.sendPayload(outcome, settled);
+        return this;
+    }
+
+    @Override
+    public ClientTransactionContext disposition(IncomingDelivery delivery, DeliveryState outcome, boolean settled) {
+        delivery.disposition(outcome, settled);
+        return this;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientOutgoingEnvelope.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientOutgoingEnvelope.java
new file mode 100644
index 0000000..5d4a95c
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientOutgoingEnvelope.java
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.ScheduledFuture;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientSendTimedOutException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+/**
+ * Tracking object used to manage the life-cycle of a send of message payload
+ * to the remote which can be stalled either for link or session credit limits.
+ * The envelope carries sufficient information to write payload bytes as credit
+ * is available.
+ */
+public class ClientOutgoingEnvelope {
+
+    private final ProtonBuffer payload;
+    private final ClientFuture<Tracker> request;
+    private final ClientSender sender;
+    private final boolean complete;
+    private final int messageFormat;
+
+    private boolean aborted;
+    private ScheduledFuture<?> sendTimeout;
+    private OutgoingDelivery delivery;
+
+    /**
+     * Create a new In-flight Send instance for a complete message send.  No further
+     * sends can occur after the send completes.
+     *
+     * @param sender
+     *      The {@link ClientSender} instance that is attempting to send this encoded message.
+     * @param messageFormat
+     *      The message format code to assign the send if this is the first delivery.
+     * @param payload
+     *      The payload that comprises this portion of the send.
+     * @param request
+     *      The requesting operation that initiated this send.
+     */
+    public ClientOutgoingEnvelope(ClientSender sender, int messageFormat, ProtonBuffer payload, ClientFuture<Tracker> request) {
+        this.messageFormat = messageFormat;
+        this.payload = payload;
+        this.request = request;
+        this.sender = sender;
+        this.complete = true;
+    }
+
+    /**
+     * Create a new In-flight Send instance.
+     *
+     * @param sender
+     *      The {@link ClientSender} instance that is attempting to send this encoded message.
+     * @param messageFormat
+     *      The message format code to assign the send if this is the first delivery.
+     * @param payload
+     *      The payload that comprises this portion of the send.
+     * @param complete
+     *      Indicates if the encoded payload represents the complete transfer or if more is coming.
+     * @param request
+     *      The requesting operation that initiated this send.
+     */
+    public ClientOutgoingEnvelope(ClientSender sender, int messageFormat, ProtonBuffer payload, boolean complete, ClientFuture<Tracker> request) {
+        this.payload = payload;
+        this.request = request;
+        this.sender = sender;
+        this.complete = complete;
+        this.messageFormat = messageFormat;
+    }
+
+    /**
+     * Create a new In-flight Send instance that is a continuation on an existing delivery.
+     *
+     * @param sender
+     *      The {@link ClientSender} instance that is attempting to send this encoded message.
+     * @param messageFormat
+     *      The message format code to assign the send if this is the first delivery.
+     * @param delivery
+     *      The {@link OutgoingDelivery} context this envelope will be added to.
+     * @param payload
+     *      The payload that comprises this portion of the send.
+     * @param complete
+     *      Indicates if the encoded payload represents the complete transfer or if more is coming.
+     * @param request
+     *      The requesting operation that initiated this send.
+     */
+    public ClientOutgoingEnvelope(ClientSender sender, OutgoingDelivery delivery, int messageFormat, ProtonBuffer payload, boolean complete, ClientFuture<Tracker> request) {
+        this.payload = payload;
+        this.request = request;
+        this.sender = sender;
+        this.complete = complete;
+        this.messageFormat = messageFormat;
+        this.delivery = delivery;
+    }
+
+    public ScheduledFuture<?> sendTimeout() {
+        return sendTimeout;
+    }
+
+    public void sendTimeout(ScheduledFuture<?> sendTimeout) {
+        this.sendTimeout = sendTimeout;
+    }
+
+    public ProtonBuffer payload() {
+        return payload;
+    }
+
+    public OutgoingDelivery delivery() {
+        return delivery;
+    }
+
+    public ClientOutgoingEnvelope abort() {
+        this.aborted = true;
+        return this;
+    }
+
+    public ClientSender sender() {
+        return sender;
+    }
+
+    public boolean aborted() {
+        return aborted;
+    }
+
+    public ClientOutgoingEnvelope discard() {
+        if (sendTimeout != null) {
+            sendTimeout.cancel(true);
+            sendTimeout = null;
+        }
+
+        if (delivery != null) {
+            ClientTracker tracker = delivery.getLinkedResource();
+            if (tracker != null) {
+                tracker.settlementFuture().complete(tracker);
+            }
+            request.complete(delivery.getLinkedResource());
+        } else {
+            request.complete(sender.createNoOpTracker());
+        }
+
+        return this;
+    }
+
+    public ClientOutgoingEnvelope succeeded() {
+        if (sendTimeout != null) {
+            sendTimeout.cancel(true);
+        }
+
+        request.complete(delivery.getLinkedResource());
+
+        return this;
+    }
+
+    public ClientOutgoingEnvelope failed(ClientException exception) {
+        if (sendTimeout != null) {
+            sendTimeout.cancel(true);
+        }
+
+        request.failed(exception);
+
+        return this;
+    }
+
+    public void sendPayload(DeliveryState state, boolean settled) {
+        if (delivery == null) {
+            delivery = sender.getProtonSender().next();
+            delivery.setLinkedResource(sender.createTracker(delivery));
+        }
+
+        if (delivery.getTransferCount() == 0) {
+            delivery.setMessageFormat(messageFormat);
+            delivery.disposition(state, settled);
+        }
+
+        // We must check if the delivery was fully written and then complete the send operation otherwise
+        // if the session capacity limited the amount of payload data we need to hold the completion until
+        // the session capacity is refilled and we can fully write the remaining message payload.  This
+        // area could use some enhancement to allow control of write and flush when dealing with delivery
+        // modes that have low assurance versus those that are strict.
+        if (aborted()) {
+            delivery.abort();
+            succeeded();
+        } else {
+            sender.connection().autoFlushOff();
+            try {
+                delivery.streamBytes(payload, complete);
+                if (payload != null && payload.isReadable()) {
+                    sender.addToHeadOfBlockedQueue(this);
+                } else {
+                    succeeded();
+                }
+                sender.connection().flush();
+            } finally {
+                sender.connection().autoFlushOn();
+            }
+        }
+    }
+
+    public ClientException createSendTimedOutException() {
+        return new ClientSendTimedOutException("Timed out waiting for credit to send");
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientReceiver.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientReceiver.java
new file mode 100644
index 0000000..0c3dfc0
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientReceiver.java
@@ -0,0 +1,658 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Source;
+import org.apache.qpid.protonj2.client.Target;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientResourceRemotelyClosedException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.client.util.FifoDeliveryQueue;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ClientReceiver implements Receiver {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientReceiver.class);
+
+    private static final AtomicIntegerFieldUpdater<ClientReceiver> CLOSED_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(ClientReceiver.class, "closed");
+
+    private final ClientFuture<Receiver> openFuture;
+    private final ClientFuture<Receiver> closeFuture;
+    private ClientFuture<Receiver> drainingFuture;
+    private ScheduledFuture<?> drainingTimeout;
+
+    private final ReceiverOptions options;
+    private final ClientSession session;
+    private final ScheduledExecutorService executor;
+    private final String receiverId;
+    private final FifoDeliveryQueue messageQueue;
+    private volatile int closed;
+    private ClientException failureCause;
+
+    private org.apache.qpid.protonj2.engine.Receiver protonReceiver;
+
+    private volatile Source remoteSource;
+    private volatile Target remoteTarget;
+
+    public ClientReceiver(ClientSession session, ReceiverOptions options, String receiverId, org.apache.qpid.protonj2.engine.Receiver receiver) {
+        this.options = options;
+        this.session = session;
+        this.receiverId = receiverId;
+        this.executor = session.getScheduler();
+        this.openFuture = session.getFutureFactory().createFuture();
+        this.closeFuture = session.getFutureFactory().createFuture();
+        this.protonReceiver = receiver.setLinkedResource(this);
+
+        if (options.creditWindow() > 0) {
+            protonReceiver.addCredit(options.creditWindow());
+        }
+
+        messageQueue = new FifoDeliveryQueue(options.creditWindow());
+        messageQueue.start();
+    }
+
+    @Override
+    public String address() throws ClientException {
+        if (isDynamic()) {
+            waitForOpenToComplete();
+            return protonReceiver.getRemoteSource().getAddress();
+        } else {
+            return protonReceiver.getSource() != null ? protonReceiver.getSource().getAddress() : null;
+        }
+    }
+
+    @Override
+    public Source source() throws ClientException {
+        waitForOpenToComplete();
+        return remoteSource;
+    }
+
+    @Override
+    public Target target() throws ClientException {
+        waitForOpenToComplete();
+        return remoteTarget;
+    }
+
+    @Override
+    public ClientInstance client() {
+        return session.client();
+    }
+
+    @Override
+    public ClientConnection connection() {
+        return session.connection();
+    }
+
+    @Override
+    public ClientSession session() {
+        return session;
+    }
+
+    @Override
+    public Future<Receiver> openFuture() {
+        return openFuture;
+    }
+
+    @Override
+    public Delivery receive() throws ClientException {
+        return receive(-1, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public Delivery receive(long timeout, TimeUnit units) throws ClientException {
+        checkClosedOrFailed();
+
+        try {
+            ClientDelivery delivery = messageQueue.dequeue(units.toMillis(timeout));
+            if (delivery != null) {
+                if (options.autoAccept()) {
+                    delivery.disposition(org.apache.qpid.protonj2.client.DeliveryState.accepted(), options.autoSettle());
+                } else {
+                    asyncReplenishCreditIfNeeded();
+                }
+
+                return delivery;
+            }
+
+            checkClosedOrFailed();
+
+            return null;
+        } catch (InterruptedException e) {
+            Thread.interrupted();
+            throw new ClientException("Receive wait interrupted", e);
+        }
+    }
+
+    @Override
+    public Delivery tryReceive() throws ClientException {
+        checkClosedOrFailed();
+
+        Delivery delivery = messageQueue.dequeueNoWait();
+        if (delivery != null) {
+            if (options.autoAccept()) {
+                delivery.disposition(org.apache.qpid.protonj2.client.DeliveryState.accepted(), options.autoSettle());
+            } else {
+                asyncReplenishCreditIfNeeded();
+            }
+        } else {
+            checkClosedOrFailed();
+        }
+
+        return delivery;
+    }
+
+    @Override
+    public void close() {
+        try {
+            doCloseOrDetach(true, null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void close(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        try {
+            doCloseOrDetach(true, error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void detach() {
+        try {
+            doCloseOrDetach(false, null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void detach(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        try {
+            doCloseOrDetach(false, error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public ClientFuture<Receiver> closeAsync() {
+        return doCloseOrDetach(true, null);
+    }
+
+    @Override
+    public ClientFuture<Receiver> closeAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        return doCloseOrDetach(true, error);
+    }
+
+    @Override
+    public ClientFuture<Receiver> detachAsync() {
+        return doCloseOrDetach(false, null);
+    }
+
+    @Override
+    public ClientFuture<Receiver> detachAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "The provided Error Condition cannot be null");
+
+        return doCloseOrDetach(false, error);
+    }
+
+    private ClientFuture<Receiver> doCloseOrDetach(boolean close, ErrorCondition error) {
+        if (CLOSED_UPDATER.compareAndSet(this, 0, 1)) {
+            if (!closeFuture.isDone()) {
+                executor.execute(() -> {
+                    if (protonReceiver.isLocallyOpen()) {
+                        try {
+                            protonReceiver.setCondition(ClientErrorCondition.asProtonErrorCondition(error));
+
+                            if (close) {
+                                protonReceiver.close();
+                            } else {
+                                protonReceiver.detach();
+                            }
+                        } catch (Throwable ignore) {
+                            closeFuture.complete(this);
+                        }
+                    }
+                });
+            }
+        }
+
+        return closeFuture;
+    }
+
+    @Override
+    public long queuedDeliveries() {
+        return messageQueue.size();
+    }
+
+    @Override
+    public Receiver addCredit(int credits) throws ClientException {
+        checkClosedOrFailed();
+        ClientFuture<Receiver> creditAdded = session.getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(creditAdded)) {
+                if (options.creditWindow() != 0) {
+                    creditAdded.failed(new ClientIllegalStateException("Cannot add credit when a credit window has been configured"));
+                } else if (protonReceiver.isDraining()) {
+                    creditAdded.failed(new ClientIllegalStateException("Cannot add credit while a drain is pending"));
+                } else {
+                    try {
+                        protonReceiver.addCredit(credits);
+                        creditAdded.complete(this);
+                    } catch (Exception ex) {
+                        creditAdded.failed(ClientExceptionSupport.createNonFatalOrPassthrough(ex));
+                    }
+                }
+            }
+        });
+
+        return session.request(this, creditAdded);
+    }
+
+    @Override
+    public Future<Receiver> drain() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Receiver> drainComplete = session.getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(drainComplete)) {
+                if (protonReceiver.isDraining()) {
+                    drainComplete.failed(new ClientIllegalStateException("Receiver is already draining"));
+                    return;
+                }
+
+                try {
+                    if (protonReceiver.drain()) {
+                        drainingFuture = drainComplete;
+                        drainingTimeout = session.scheduleRequestTimeout(drainingFuture, options.drainTimeout(),
+                            () -> new ClientOperationTimedOutException("Timed out waiting for remote to respond to drain request"));
+                    } else {
+                        drainComplete.complete(this);
+                    }
+                } catch (Exception ex) {
+                    drainComplete.failed(ClientExceptionSupport.createNonFatalOrPassthrough(ex));
+                }
+            }
+        });
+
+        return drainComplete;
+    }
+
+    @Override
+    public Map<String, Object> properties() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringKeyedMap(protonReceiver.getRemoteProperties());
+    }
+
+    @Override
+    public String[] offeredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonReceiver.getRemoteOfferedCapabilities());
+    }
+
+    @Override
+    public String[] desiredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonReceiver.getRemoteDesiredCapabilities());
+    }
+
+    //----- Internal API for the ClientReceiver and other Client objects
+
+    void disposition(IncomingDelivery delivery, DeliveryState state, boolean settle) throws ClientException {
+        checkClosedOrFailed();
+        asyncApplyDisposition(delivery, state, settle);
+    }
+
+    ClientReceiver open() {
+        protonReceiver.localOpenHandler(this::handleLocalOpen)
+                      .localCloseHandler(this::handleLocalCloseOrDetach)
+                      .localDetachHandler(this::handleLocalCloseOrDetach)
+                      .openHandler(this::handleRemoteOpen)
+                      .closeHandler(this::handleRemoteCloseOrDetach)
+                      .detachHandler(this::handleRemoteCloseOrDetach)
+                      .parentEndpointClosedHandler(this::handleParentEndpointClosed)
+                      .deliveryStateUpdatedHandler(this::handleDeliveryStateRemotelyUpdated)
+                      .deliveryReadHandler(this::handleDeliveryReceived)
+                      .deliveryAbortedHandler(this::handleDeliveryAborted)
+                      .creditStateUpdateHandler(this::handleReceiverCreditUpdated)
+                      .engineShutdownHandler(this::handleEngineShutdown)
+                      .open();
+
+        return this;
+    }
+
+    void setFailureCause(ClientException failureCause) {
+        this.failureCause = failureCause;
+    }
+
+    ClientException getFailureCause() {
+        if (failureCause == null) {
+            return session.getFailureCause();
+        } else {
+            return failureCause;
+        }
+    }
+
+    String getId() {
+        return receiverId;
+    }
+
+    boolean isClosed() {
+        return closed > 0;
+    }
+
+    boolean isDynamic() {
+        return protonReceiver.getSource() != null && protonReceiver.getSource().isDynamic();
+    }
+
+    //----- Handlers for proton receiver events
+
+    private void handleLocalOpen(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        if (options.openTimeout() > 0) {
+            executor.schedule(() -> {
+                if (!openFuture.isDone()) {
+                    immediateLinkShutdown(new ClientOperationTimedOutException("Receiver open timed out waiting for remote to respond"));
+                }
+            }, options.openTimeout(), TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void handleLocalCloseOrDetach(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        messageQueue.stop();  // Ensure blocked receivers are all unblocked.
+
+        // If not yet remotely closed we only wait for a remote close if the engine isn't
+        // already failed and we have successfully opened the sender without a timeout.
+        if (!receiver.getEngine().isShutdown() && failureCause == null && receiver.isRemotelyOpen()) {
+            final long timeout = options.closeTimeout();
+
+            if (timeout > 0) {
+                session.scheduleRequestTimeout(closeFuture, timeout, () ->
+                new ClientOperationTimedOutException("receiver close timed out waiting for remote to respond"));
+            }
+        } else {
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleRemoteOpen(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        // Check for deferred close pending and hold completion if so
+        if (receiver.getRemoteSource() != null) {
+            remoteSource = new ClientRemoteSource(receiver.getRemoteSource());
+
+            if (receiver.getRemoteTarget() != null) {
+                remoteTarget = new ClientRemoteTarget(receiver.getRemoteTarget());
+            }
+
+            replenishCreditIfNeeded();
+
+            openFuture.complete(this);
+            LOG.trace("Receiver opened successfully: {}", receiverId);
+        } else {
+            LOG.debug("Receiver opened but remote signalled close is pending: {}", receiverId);
+        }
+    }
+
+    private void handleRemoteCloseOrDetach(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        if (receiver.isLocallyOpen()) {
+            immediateLinkShutdown(ClientExceptionSupport.convertToLinkClosedException(
+                receiver.getRemoteCondition(), "Receiver remotely closed without explanation from the remote"));
+        } else {
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleParentEndpointClosed(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        // Don't react if engine was shutdown and parent closed as a result instead wait to get the
+        // shutdown notification and respond to that change.
+        if (receiver.getEngine().isRunning()) {
+            final ClientException failureCause;
+
+            if (receiver.getConnection().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(receiver.getConnection().getRemoteCondition());
+            } else if (receiver.getSession().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToSessionClosedException(receiver.getSession().getRemoteCondition());
+            } else if (receiver.getEngine().failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(receiver.getEngine().failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientResourceRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        if (!isDynamic() && !session.getConnection().getEngine().isShutdown()) {
+            int previousCredit = protonReceiver.getCredit() + messageQueue.size();
+
+            messageQueue.clear();  // Prefetched messages should be discarded.
+
+            if (drainingFuture != null) {
+                drainingFuture.complete(this);
+                if (drainingTimeout != null) {
+                    drainingTimeout.cancel(false);
+                    drainingTimeout = null;
+                }
+            }
+
+            protonReceiver.localCloseHandler(null);
+            protonReceiver.localDetachHandler(null);
+            protonReceiver.close();
+            protonReceiver = ClientReceiverBuilder.recreateReceiver(session, protonReceiver, options);
+            protonReceiver.setLinkedResource(this);
+            protonReceiver.addCredit(previousCredit);
+
+            open();
+        } else {
+            final Connection connection = engine.connection();
+
+            final ClientException failureCause;
+
+            if (connection.getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(connection.getRemoteCondition());
+            } else if (engine.failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(engine.failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientConnectionRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleDeliveryReceived(IncomingDelivery delivery) {
+        LOG.trace("Delivery data was received: {}", delivery);
+
+        if (delivery.getDefaultDeliveryState() == null) {
+            delivery.setDefaultDeliveryState(Released.getInstance());
+        }
+
+        if (!delivery.isPartial()) {
+            LOG.trace("{} has incoming Message(s).", this);
+            messageQueue.enqueue(new ClientDelivery(this, delivery));
+        } else {
+            delivery.claimAvailableBytes();
+        }
+    }
+
+    private void handleDeliveryAborted(IncomingDelivery delivery) {
+        LOG.trace("Delivery data was aborted: {}", delivery);
+        delivery.settle();
+        replenishCreditIfNeeded();
+    }
+
+    private void handleDeliveryStateRemotelyUpdated(IncomingDelivery delivery) {
+        LOG.trace("Delivery remote state was updated: {}", delivery);
+    }
+
+    private void handleReceiverCreditUpdated(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        LOG.trace("Receiver credit update by remote: {}", receiver);
+
+        if (drainingFuture != null) {
+            if (receiver.getCredit() == 0) {
+                drainingFuture.complete(this);
+                if (drainingTimeout != null) {
+                    drainingTimeout.cancel(false);
+                    drainingTimeout = null;
+                }
+            }
+        }
+    }
+
+    //----- Private implementation details
+
+    private void asyncApplyDisposition(IncomingDelivery delivery, DeliveryState state, boolean settle) {
+        executor.execute(() -> {
+            session.getTransactionContext().disposition(delivery, state, settle);
+            replenishCreditIfNeeded();
+        });
+    }
+
+    private void replenishCreditIfNeeded() {
+        int creditWindow = options.creditWindow();
+        if (creditWindow > 0) {
+            int currentCredit = protonReceiver.getCredit();
+            if (currentCredit <= creditWindow * 0.5) {
+                int potentialPrefetch = currentCredit + messageQueue.size();
+
+                if (potentialPrefetch <= creditWindow * 0.7) {
+                    int additionalCredit = creditWindow - potentialPrefetch;
+
+                    LOG.trace("Consumer granting additional credit: {}", additionalCredit);
+                    try {
+                        protonReceiver.addCredit(additionalCredit);
+                    } catch (Exception ex) {
+                        LOG.debug("Error caught during credit top-up", ex);
+                    }
+                }
+            }
+        }
+    }
+
+    private void asyncReplenishCreditIfNeeded() {
+        int creditWindow = options.creditWindow();
+        if (creditWindow > 0) {
+            executor.execute(() -> replenishCreditIfNeeded());
+        }
+    }
+
+    private void waitForOpenToComplete() throws ClientException {
+        if (!openFuture.isComplete() || openFuture.isFailed()) {
+            try {
+                openFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                Thread.interrupted();
+                if (failureCause != null) {
+                    throw failureCause;
+                } else {
+                    throw ClientExceptionSupport.createNonFatalOrPassthrough(e.getCause());
+                }
+            }
+        }
+    }
+
+    private boolean notClosedOrFailed(ClientFuture<?> request) {
+        if (isClosed()) {
+            request.failed(new ClientIllegalStateException("The Receiver was explicity closed", failureCause));
+            return false;
+        } else if (failureCause != null) {
+            request.failed(failureCause);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    protected void checkClosedOrFailed() throws ClientException {
+        if (isClosed()) {
+            throw new ClientIllegalStateException("The Receiver was explicity closed", failureCause);
+        } else if (failureCause != null) {
+            throw failureCause;
+        }
+    }
+
+    private void immediateLinkShutdown(ClientException failureCause) {
+        if (this.failureCause == null) {
+            this.failureCause = failureCause;
+        }
+
+        try {
+            if (protonReceiver.isRemotelyDetached()) {
+                protonReceiver.detach();
+            } else {
+                protonReceiver.close();
+            }
+        } catch (Exception ignore) {
+        }
+
+        if (failureCause != null) {
+            openFuture.failed(failureCause);
+            if (drainingFuture != null) {
+                drainingFuture.failed(failureCause);
+            }
+        } else {
+            openFuture.complete(this);
+            if (drainingFuture != null) {
+                drainingFuture.failed(new ClientResourceRemotelyClosedException("The Receiver has been closed"));
+            }
+        }
+
+        if (drainingTimeout != null) {
+            drainingTimeout.cancel(false);
+            drainingTimeout = null;
+        }
+
+        closeFuture.complete(this);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientReceiverBuilder.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientReceiverBuilder.java
new file mode 100644
index 0000000..bdd7402
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientReceiverBuilder.java
@@ -0,0 +1,285 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.SourceOptions;
+import org.apache.qpid.protonj2.client.StreamReceiverOptions;
+import org.apache.qpid.protonj2.client.TargetOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+
+/**
+ * Session owned builder of {@link Receiver} objects.
+ */
+final class ClientReceiverBuilder {
+
+    private final ClientSession session;
+    private final SessionOptions sessionOptions;
+    private final AtomicInteger receiverCounter = new AtomicInteger();
+
+    private ReceiverOptions defaultReceivernOptions;
+    private StreamReceiverOptions defaultStreamReceiverOptions;
+
+    public ClientReceiverBuilder(ClientSession session) {
+        this.session = session;
+        this.sessionOptions = session.options();
+    }
+
+    public ClientReceiver receiver(String address, ReceiverOptions receiverOptions) throws ClientException {
+        final ReceiverOptions rcvOptions = receiverOptions != null ? receiverOptions : getDefaultReceiverOptions();
+        final String receiverId = nextReceiverId();
+        final Receiver protonReceiver = createReceiver(address, rcvOptions, receiverId);
+
+        protonReceiver.setSource(createSource(address, rcvOptions));
+        protonReceiver.setTarget(createTarget(address, rcvOptions));
+
+        return new ClientReceiver(session, rcvOptions, receiverId, protonReceiver);
+    }
+
+    public ClientReceiver durableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) {
+        final ReceiverOptions options = receiverOptions != null ? receiverOptions : getDefaultReceiverOptions();
+        final String receiverId = nextReceiverId();
+
+        options.linkName(subscriptionName);
+
+        final Receiver protonReceiver = createReceiver(address, options, receiverId);
+
+        protonReceiver.setSource(createDurableSource(address, options));
+        protonReceiver.setTarget(createTarget(address, options));
+
+        return new ClientReceiver(session, options, receiverId, protonReceiver);
+    }
+
+    public ClientReceiver dynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException {
+        final ReceiverOptions options = receiverOptions != null ? receiverOptions : getDefaultReceiverOptions();
+        final String receiverId = nextReceiverId();
+        final Receiver protonReceiver = createReceiver(null, options, receiverId);
+
+        protonReceiver.setSource(createSource(null, options));
+        protonReceiver.setTarget(createTarget(null, options));
+
+        // Configure the dynamic nature of the source now.
+        protonReceiver.getSource().setDynamic(true);
+        protonReceiver.getSource().setDynamicNodeProperties(ClientConversionSupport.toSymbolKeyedMap(dynamicNodeProperties));
+
+        return new ClientReceiver(session, options, receiverId, protonReceiver);
+    }
+
+    public ClientStreamReceiver streamReceiver(String address, StreamReceiverOptions receiverOptions) throws ClientException {
+        final StreamReceiverOptions options = receiverOptions != null ? receiverOptions : getDefaultStreamReceiverOptions();
+        final String receiverId = nextReceiverId();
+        final Receiver protonReceiver = createReceiver(address, options, receiverId);
+
+        protonReceiver.setSource(createSource(address, options));
+        protonReceiver.setTarget(createTarget(address, options));
+
+        return new ClientStreamReceiver(session, options, receiverId, protonReceiver);
+    }
+
+    public static Receiver recreateReceiver(ClientSession session, Receiver previousReceiver, ReceiverOptions options) {
+        final Receiver protonReceiver = session.getProtonSession().receiver(previousReceiver.getName());
+
+        protonReceiver.setSource(previousReceiver.getSource());
+        if (previousReceiver.getTarget() instanceof Coordinator) {
+            protonReceiver.setTarget((Coordinator) previousReceiver.getTarget());
+        } else {
+            protonReceiver.setTarget((Target) previousReceiver.getTarget());
+        }
+
+        protonReceiver.setSenderSettleMode(previousReceiver.getSenderSettleMode());
+        protonReceiver.setReceiverSettleMode(previousReceiver.getReceiverSettleMode());
+        protonReceiver.setOfferedCapabilities(ClientConversionSupport.toSymbolArray(options.offeredCapabilities()));
+        protonReceiver.setDesiredCapabilities(ClientConversionSupport.toSymbolArray(options.desiredCapabilities()));
+        protonReceiver.setProperties(ClientConversionSupport.toSymbolKeyedMap(options.properties()));
+        protonReceiver.setDefaultDeliveryState(Released.getInstance());
+
+        return protonReceiver;
+    }
+
+    private String nextReceiverId() {
+        return session.id() + ":" + receiverCounter.incrementAndGet();
+    }
+
+    private Receiver createReceiver(String address, ReceiverOptions options, String receiverId) {
+        final String linkName;
+
+        if (options.linkName() != null) {
+            linkName = options.linkName();
+        } else {
+            linkName = "receiver-" + receiverId;
+        }
+
+        final Receiver protonReceiver = session.getProtonSession().receiver(linkName);
+
+        switch (options.deliveryMode()) {
+            case AT_MOST_ONCE:
+                protonReceiver.setSenderSettleMode(SenderSettleMode.SETTLED);
+                protonReceiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+                break;
+            case AT_LEAST_ONCE:
+                protonReceiver.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+                protonReceiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+                break;
+        }
+
+        protonReceiver.setOfferedCapabilities(ClientConversionSupport.toSymbolArray(options.offeredCapabilities()));
+        protonReceiver.setDesiredCapabilities(ClientConversionSupport.toSymbolArray(options.desiredCapabilities()));
+        protonReceiver.setProperties(ClientConversionSupport.toSymbolKeyedMap(options.properties()));
+        protonReceiver.setDefaultDeliveryState(Released.getInstance());
+
+        return protonReceiver;
+    }
+
+    private Source createSource(String address, ReceiverOptions options) {
+        final SourceOptions sourceOptions = options.sourceOptions();
+
+        Source source = new Source();
+        source.setAddress(address);
+        if (sourceOptions.durabilityMode() != null) {
+            source.setDurable(ClientConversionSupport.asProtonType(sourceOptions.durabilityMode()));
+        } else {
+            source.setDurable(TerminusDurability.NONE);
+        }
+        if (sourceOptions.expiryPolicy() != null) {
+            source.setExpiryPolicy(ClientConversionSupport.asProtonType(sourceOptions.expiryPolicy()));
+        } else {
+            source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH);
+        }
+        if (sourceOptions.distributionMode() != null) {
+            source.setDistributionMode(ClientConversionSupport.asProtonType(sourceOptions.distributionMode()));
+        }
+        if (sourceOptions.timeout() >= 0) {
+            source.setTimeout(UnsignedInteger.valueOf(sourceOptions.timeout()));
+        }
+        if (sourceOptions.filters() != null) {
+            source.setFilter(ClientConversionSupport.toSymbolKeyedMap(sourceOptions.filters()));
+        }
+        if (sourceOptions.defaultOutcome() != null) {
+            source.setDefaultOutcome((Outcome) ClientDeliveryState.asProtonType(sourceOptions.defaultOutcome()));
+        } else {
+            source.setDefaultOutcome((Outcome) ClientDeliveryState.asProtonType(SourceOptions.DEFAULT_RECEIVER_OUTCOME));
+        }
+
+        source.setOutcomes(ClientConversionSupport.outcomesToSymbols(sourceOptions.outcomes()));
+        source.setCapabilities(ClientConversionSupport.toSymbolArray(sourceOptions.capabilities()));
+
+        return source;
+    }
+
+    private Source createDurableSource(String address, ReceiverOptions options) {
+        final SourceOptions sourceOptions = options.sourceOptions();
+        final Source source = new Source();
+
+        source.setAddress(address);
+        source.setDurable(TerminusDurability.UNSETTLED_STATE);
+        source.setExpiryPolicy(TerminusExpiryPolicy.NEVER);
+        source.setDistributionMode(ClientConstants.COPY);
+        source.setOutcomes(ClientConversionSupport.outcomesToSymbols(sourceOptions.outcomes()));
+        source.setDefaultOutcome((Outcome) ClientDeliveryState.asProtonType(sourceOptions.defaultOutcome()));
+        source.setCapabilities(ClientConversionSupport.toSymbolArray(sourceOptions.capabilities()));
+
+        if (sourceOptions.timeout() >= 0) {
+            source.setTimeout(UnsignedInteger.valueOf(sourceOptions.timeout()));
+        }
+        if (sourceOptions.filters() != null) {
+            source.setFilter(ClientConversionSupport.toSymbolKeyedMap(sourceOptions.filters()));
+        }
+
+        return source;
+    }
+
+    private Target createTarget(String address, ReceiverOptions options) {
+        final TargetOptions targetOptions = options.targetOptions();
+        final Target target = new Target();
+
+        target.setAddress(address);
+        target.setCapabilities(ClientConversionSupport.toSymbolArray(targetOptions.capabilities()));
+
+        if (targetOptions.durabilityMode() != null) {
+            target.setDurable(ClientConversionSupport.asProtonType(targetOptions.durabilityMode()));
+        }
+        if (targetOptions.expiryPolicy() != null) {
+            target.setExpiryPolicy(ClientConversionSupport.asProtonType(targetOptions.expiryPolicy()));
+        }
+        if (targetOptions.timeout() >= 0) {
+            target.setTimeout(UnsignedInteger.valueOf(targetOptions.timeout()));
+        }
+
+        return target;
+    }
+
+    /*
+     * Receiver options used when none specified by the caller creating a new receiver.
+     */
+    private ReceiverOptions getDefaultReceiverOptions() {
+        ReceiverOptions receiverOptions = defaultReceivernOptions;
+        if (receiverOptions == null) {
+            synchronized (this) {
+                receiverOptions = defaultReceivernOptions;
+                if (receiverOptions == null) {
+                    receiverOptions = new ReceiverOptions();
+                    receiverOptions.openTimeout(sessionOptions.openTimeout());
+                    receiverOptions.closeTimeout(sessionOptions.closeTimeout());
+                    receiverOptions.requestTimeout(sessionOptions.requestTimeout());
+                    receiverOptions.drainTimeout(sessionOptions.drainTimeout());
+                }
+
+                defaultReceivernOptions = receiverOptions;
+            }
+        }
+
+        return receiverOptions;
+    }
+
+    /*
+     * Stream Receiver options used when none specified by the caller creating a new receiver.
+     */
+    private StreamReceiverOptions getDefaultStreamReceiverOptions() {
+        StreamReceiverOptions receiverOptions = defaultStreamReceiverOptions;
+        if (receiverOptions == null) {
+            synchronized (this) {
+                receiverOptions = defaultStreamReceiverOptions;
+                if (receiverOptions == null) {
+                    receiverOptions = new StreamReceiverOptions();
+                    receiverOptions.openTimeout(sessionOptions.openTimeout());
+                    receiverOptions.closeTimeout(sessionOptions.closeTimeout());
+                    receiverOptions.requestTimeout(sessionOptions.requestTimeout());
+                    receiverOptions.drainTimeout(sessionOptions.drainTimeout());
+                }
+
+                defaultStreamReceiverOptions = receiverOptions;
+            }
+        }
+
+        return receiverOptions;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRedirect.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRedirect.java
new file mode 100644
index 0000000..d6c9c22
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRedirect.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.apache.qpid.protonj2.client.impl.ClientConstants.ADDRESS;
+import static org.apache.qpid.protonj2.client.impl.ClientConstants.NETWORK_HOST;
+import static org.apache.qpid.protonj2.client.impl.ClientConstants.OPEN_HOSTNAME;
+import static org.apache.qpid.protonj2.client.impl.ClientConstants.PATH;
+import static org.apache.qpid.protonj2.client.impl.ClientConstants.PORT;
+import static org.apache.qpid.protonj2.client.impl.ClientConstants.SCHEME;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Encapsulates the AMQP Redirect Map
+ */
+public final class ClientRedirect {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientRedirect.class);
+
+    private final Map<Symbol, Object> redirect;
+
+    private URI cachedURI;
+
+    public ClientRedirect(Map<Symbol, Object> redirect) {
+        this.redirect = redirect;
+    }
+
+    public ClientRedirect validate() throws Exception {
+        String networkHost = (String) redirect.get(NETWORK_HOST);
+        if (networkHost == null || networkHost.isEmpty()) {
+            throw new IOException("Redirection information not set, missing network host.");
+        }
+
+        final int networkPort;
+        try {
+            networkPort = Integer.parseInt(redirect.get(PORT).toString());
+        } catch (Exception ex) {
+            throw new IOException("Redirection information contained invalid port.");
+        }
+
+        LOG.trace("Redirect issued host and port as follows: {}:{}", networkHost, networkPort);
+
+        // Check it actually converts to URI since we require it do so later
+        cachedURI = toURI();
+
+        return this;
+    }
+
+    /**
+     * @return the redirection map that backs this object
+     */
+    public Map<Symbol, Object> getRedirectMap() {
+        return redirect;
+    }
+
+    /**
+     * @return the host name of the container being redirected to.
+     */
+    public String getHostname() {
+        return (String) redirect.get(OPEN_HOSTNAME);
+    }
+
+    /**
+     * @return the DNS host name or IP address of the peer this connection is being redirected to.
+     */
+    public String getNetworkHost() {
+        return (String) redirect.get(NETWORK_HOST);
+    }
+
+    /**
+     * @return the port number on the peer this connection is being redirected to.
+     */
+    public int getPort() {
+        return Integer.parseInt(redirect.get(PORT).toString());
+    }
+
+    /**
+     * @return the scheme that the remote indicated the redirect connection should use.
+     */
+    public String getScheme() {
+        return (String) redirect.get(SCHEME);
+    }
+
+    /**
+     * @return the path that the remote indicated should be path of the redirect URI.
+     */
+    public String getPath() {
+        return (String) redirect.get(PATH);
+    }
+
+    /**
+     * @return the address that the remote indicated should be used for link redirection.
+     */
+    public String getAddress() {
+        return (String) redirect.get(ADDRESS);
+    }
+
+    /**
+     * Construct a URI from the redirection information available.
+     *
+     * @return a URI that matches the redirection information provided.
+     *
+     * @throws Exception if an error occurs construct a URI from the redirection information.
+     */
+    public URI toURI() throws Exception {
+        if (cachedURI != null) {
+            return cachedURI;
+        } else {
+            return cachedURI = new URI(getScheme(), null, getNetworkHost(), getPort(), getPath(), null, null);
+        }
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return toURI().toString();
+        } catch (Exception ex) {
+            return "<Invalid-Redirect-Value>";
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRemoteSource.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRemoteSource.java
new file mode 100644
index 0000000..ac16fa6
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRemoteSource.java
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.DistributionMode;
+import org.apache.qpid.protonj2.client.DurabilityMode;
+import org.apache.qpid.protonj2.client.ExpiryPolicy;
+import org.apache.qpid.protonj2.client.Source;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Wrapper around a remote {@link Source} that provides read-only accees to
+ * the remote Source configuration.
+ */
+final class ClientRemoteSource implements Source {
+
+    private final org.apache.qpid.protonj2.types.messaging.Source remoteSource;
+
+    private DeliveryState cachedDefaultOutcome;
+    private DistributionMode cachedDistributionMode;
+    private Map<String, Object> cachedDynamicNodeProperties;
+    private Map<String, String> cachedFilters;
+    private Set<DeliveryState.Type> cachedOutcomes;
+    private Set<String> cachedCapabilities;
+
+    ClientRemoteSource(org.apache.qpid.protonj2.types.messaging.Source remoteSource) {
+        this.remoteSource = remoteSource;
+    }
+
+    @Override
+    public String address() {
+        return remoteSource.getAddress();
+    }
+
+    @Override
+    public DurabilityMode durabilityMode() {
+        if (remoteSource.getDurable() != null) {
+            switch (remoteSource.getDurable()) {
+                case NONE:
+                    return DurabilityMode.NONE;
+                case CONFIGURATION:
+                    return DurabilityMode.CONFIGURATION;
+                case UNSETTLED_STATE:
+                    return DurabilityMode.UNSETTLED_STATE;
+            }
+        }
+
+        return DurabilityMode.NONE;
+    }
+
+    @Override
+    public long timeout() {
+        return remoteSource.getTimeout() == null ? 0 : remoteSource.getTimeout().longValue();
+    }
+
+    @Override
+    public ExpiryPolicy expiryPolicy() {
+        if (remoteSource.getExpiryPolicy() != null) {
+            switch (remoteSource.getExpiryPolicy()) {
+            case LINK_DETACH:
+                return ExpiryPolicy.LINK_CLOSE;
+            case SESSION_END:
+                return ExpiryPolicy.SESSION_CLOSE;
+            case CONNECTION_CLOSE:
+                return ExpiryPolicy.CONNECTION_CLOSE;
+            case NEVER:
+                return ExpiryPolicy.NEVER;
+            }
+        }
+
+        return ExpiryPolicy.SESSION_CLOSE;
+    }
+
+    @Override
+    public boolean dynamic() {
+        return remoteSource.isDynamic();
+    }
+
+    @Override
+    public Map<String, Object> dynamicNodeProperties() {
+        if (cachedDynamicNodeProperties == null && remoteSource.getDynamicNodeProperties() != null) {
+            cachedDynamicNodeProperties =
+                Collections.unmodifiableMap(ClientConversionSupport.toStringKeyedMap(remoteSource.getDynamicNodeProperties()));
+        }
+
+        return cachedDynamicNodeProperties;
+    }
+
+    @Override
+    public DistributionMode distributionMode() {
+        if (cachedDistributionMode == null && remoteSource.getDistributionMode() != null) {
+            switch (remoteSource.getDistributionMode().toString()) {
+                case "MOVE":
+                    cachedDistributionMode = DistributionMode.MOVE;
+                    break;
+                case "COPY":
+                    cachedDistributionMode = DistributionMode.COPY;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return cachedDistributionMode;
+    }
+
+    @Override
+    public Map<String, String> filters() {
+        if (cachedFilters == null && remoteSource.getFilter() != null) {
+            final Map<String, String> result = cachedFilters = new LinkedHashMap<>();
+            remoteSource.getFilter().forEach((key, value) -> {
+                result.put(key.toString(), value.toString());
+            });
+        }
+
+        return cachedFilters;
+    }
+
+    @Override
+    public DeliveryState defaultOutcome() {
+        if (cachedDefaultOutcome == null && remoteSource.getDefaultOutcome() != null) {
+            cachedDefaultOutcome = ClientDeliveryState.fromProtonType(remoteSource.getDefaultOutcome());
+        }
+
+        return cachedDefaultOutcome;
+    }
+
+    @Override
+    public Set<DeliveryState.Type> outcomes() {
+        if (cachedOutcomes == null && remoteSource.getOutcomes() != null) {
+            cachedOutcomes = new LinkedHashSet<>(remoteSource.getOutcomes().length);
+            for (Symbol outcomeName : remoteSource.getOutcomes()) {
+                cachedOutcomes.add(ClientDeliveryState.fromOutcomeSymbol(outcomeName));
+            }
+
+            cachedOutcomes = Collections.unmodifiableSet(cachedOutcomes);
+        }
+
+        return cachedOutcomes;
+    }
+
+    @Override
+    public Set<String> capabilities() {
+        if (cachedCapabilities == null && remoteSource.getCapabilities() != null) {
+            cachedCapabilities = Collections.unmodifiableSet(ClientConversionSupport.toStringSet(remoteSource.getCapabilities()));
+        }
+
+        return cachedCapabilities;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRemoteTarget.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRemoteTarget.java
new file mode 100644
index 0000000..e90be27
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientRemoteTarget.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.client.DurabilityMode;
+import org.apache.qpid.protonj2.client.ExpiryPolicy;
+import org.apache.qpid.protonj2.client.Target;
+
+/**
+ * Wrapper around a remote {@link Target} that provides read-only access to
+ * the remote Target configuration.
+ */
+final class ClientRemoteTarget implements Target {
+
+    private final org.apache.qpid.protonj2.types.messaging.Target remoteTarget;
+
+    private Map<String, Object> cachedDynamicNodeProperties;
+    private Set<String> cachedCapabilities;
+
+    ClientRemoteTarget(org.apache.qpid.protonj2.types.messaging.Target remoteTarget) {
+        this.remoteTarget = remoteTarget;
+    }
+
+    @Override
+    public String address() {
+        return remoteTarget.getAddress();
+    }
+
+    @Override
+    public DurabilityMode durabilityMode() {
+        if (remoteTarget.getDurable() != null) {
+            switch (remoteTarget.getDurable()) {
+                case NONE:
+                    return DurabilityMode.NONE;
+                case CONFIGURATION:
+                    return DurabilityMode.CONFIGURATION;
+                case UNSETTLED_STATE:
+                    return DurabilityMode.UNSETTLED_STATE;
+            }
+        }
+
+        return DurabilityMode.NONE;
+    }
+
+    @Override
+    public long timeout() {
+        return remoteTarget.getTimeout() == null ? 0 : remoteTarget.getTimeout().longValue();
+    }
+
+    @Override
+    public ExpiryPolicy expiryPolicy() {
+        if (remoteTarget.getExpiryPolicy() != null) {
+            switch (remoteTarget.getExpiryPolicy()) {
+            case LINK_DETACH:
+                return ExpiryPolicy.LINK_CLOSE;
+            case SESSION_END:
+                return ExpiryPolicy.SESSION_CLOSE;
+            case CONNECTION_CLOSE:
+                return ExpiryPolicy.CONNECTION_CLOSE;
+            case NEVER:
+                return ExpiryPolicy.NEVER;
+            }
+        }
+
+        return ExpiryPolicy.SESSION_CLOSE;
+    }
+
+    @Override
+    public boolean dynamic() {
+        return remoteTarget.isDynamic();
+    }
+
+    @Override
+    public Map<String, Object> dynamicNodeProperties() {
+        if (cachedDynamicNodeProperties == null && remoteTarget.getDynamicNodeProperties() != null) {
+            cachedDynamicNodeProperties =
+                Collections.unmodifiableMap(ClientConversionSupport.toStringKeyedMap(remoteTarget.getDynamicNodeProperties()));
+        }
+
+        return cachedDynamicNodeProperties;
+    }
+
+    @Override
+    public Set<String> capabilities() {
+        if (cachedCapabilities == null && remoteTarget.getCapabilities() != null) {
+            cachedCapabilities = Collections.unmodifiableSet(ClientConversionSupport.toStringSet(remoteTarget.getCapabilities()));
+        }
+
+        return cachedCapabilities;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSender.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSender.java
new file mode 100644
index 0000000..0f711b4
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSender.java
@@ -0,0 +1,701 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Source;
+import org.apache.qpid.protonj2.client.Target;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientResourceRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.client.futures.ClientSynchronization;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.LinkState;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Proton based AMQP Sender
+ */
+class ClientSender implements Sender {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientSender.class);
+
+    protected static final AtomicIntegerFieldUpdater<ClientSender> CLOSED_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(ClientSender.class, "closed");
+
+    protected final ClientFuture<Sender> openFuture;
+    protected final ClientFuture<Sender> closeFuture;
+
+    protected volatile int closed;
+    protected ClientException failureCause;
+
+    protected final Deque<ClientOutgoingEnvelope> blocked = new ArrayDeque<>();
+    protected final SenderOptions options;
+    protected final ClientSession session;
+    protected final ScheduledExecutorService executor;
+    protected final String senderId;
+    protected final boolean sendsSettled;
+    protected org.apache.qpid.protonj2.engine.Sender protonSender;
+    protected Consumer<Sender> senderRemotelyClosedHandler;
+
+    protected volatile Source remoteSource;
+    protected volatile Target remoteTarget;
+
+    public ClientSender(ClientSession session, SenderOptions options, String senderId, org.apache.qpid.protonj2.engine.Sender protonSender) {
+        this.options = new SenderOptions(options);
+        this.session = session;
+        this.senderId = senderId;
+        this.executor = session.getScheduler();
+        this.openFuture = session.getFutureFactory().createFuture();
+        this.closeFuture = session.getFutureFactory().createFuture();
+        this.protonSender = protonSender.setLinkedResource(this);
+        this.sendsSettled = protonSender.getSenderSettleMode() == SenderSettleMode.SETTLED;
+    }
+
+    @Override
+    public String address() throws ClientException {
+        final org.apache.qpid.protonj2.types.messaging.Target target;
+        if (isDynamic()) {
+            waitForOpenToComplete();
+            target = protonSender.getRemoteTarget();
+        } else {
+            target = protonSender.getTarget();
+        }
+
+        if (target != null) {
+            return target.getAddress();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Source source() throws ClientException {
+        waitForOpenToComplete();
+        return remoteSource;
+    }
+
+    @Override
+    public Target target() throws ClientException {
+        waitForOpenToComplete();
+        return remoteTarget;
+    }
+
+    @Override
+    public ClientInstance client() {
+        return session.client();
+    }
+
+    @Override
+    public ClientConnection connection() {
+        return session.connection();
+    }
+
+    @Override
+    public ClientSession session() {
+        return session;
+    }
+
+    @Override
+    public ClientFuture<Sender> openFuture() {
+        return openFuture;
+    }
+
+    @Override
+    public void close() {
+        try {
+            doCloseOrDetach(true, null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void close(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        try {
+            doCloseOrDetach(true, error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void detach() {
+        try {
+            doCloseOrDetach(false, null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void detach(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        try {
+            doCloseOrDetach(false, error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public ClientFuture<Sender> closeAsync() {
+        return doCloseOrDetach(true, null);
+    }
+
+    @Override
+    public ClientFuture<Sender> closeAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        return doCloseOrDetach(true, error);
+    }
+
+    @Override
+    public ClientFuture<Sender> detachAsync() {
+        return doCloseOrDetach(false, null);
+    }
+
+    @Override
+    public ClientFuture<Sender> detachAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        return doCloseOrDetach(false, error);
+    }
+
+    private ClientFuture<Sender> doCloseOrDetach(boolean close, ErrorCondition error) {
+        if (CLOSED_UPDATER.compareAndSet(this, 0, 1)) {
+            executor.execute(() -> {
+                if (protonSender.isLocallyOpen()) {
+                    try {
+                        protonSender.setCondition(ClientErrorCondition.asProtonErrorCondition(error));
+
+                        if (close) {
+                            protonSender.close();
+                        } else {
+                            protonSender.detach();
+                        }
+                    } catch (Throwable ignore) {
+                        closeFuture.complete(this);
+                    }
+                }
+            });
+        }
+        return closeFuture;
+    }
+
+    @Override
+    public Map<String, Object> properties() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringKeyedMap(protonSender.getRemoteProperties());
+    }
+
+    @Override
+    public String[] offeredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonSender.getRemoteOfferedCapabilities());
+    }
+
+    @Override
+    public String[] desiredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonSender.getRemoteDesiredCapabilities());
+    }
+
+    @Override
+    public Tracker send(Message<?> message) throws ClientException {
+        checkClosedOrFailed();
+        return sendMessage(ClientMessageSupport.convertMessage(message), null, true);
+    }
+
+    @Override
+    public Tracker send(Message<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        checkClosedOrFailed();
+        return sendMessage(ClientMessageSupport.convertMessage(message), deliveryAnnotations, true);
+    }
+
+    @Override
+    public Tracker trySend(Message<?> message) throws ClientException {
+        checkClosedOrFailed();
+        return sendMessage(ClientMessageSupport.convertMessage(message), null, false);
+    }
+
+    @Override
+    public Tracker trySend(Message<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        checkClosedOrFailed();
+        return sendMessage(ClientMessageSupport.convertMessage(message), deliveryAnnotations, false);
+    }
+
+    //----- Internal API
+
+    SenderOptions options() {
+        return this.options;
+    }
+
+    Sender remotelyClosedHandler(Consumer<Sender> handler) {
+        this.senderRemotelyClosedHandler = handler;
+        return this;
+    }
+
+    void disposition(OutgoingDelivery delivery, DeliveryState state, boolean settled) throws ClientException {
+        checkClosedOrFailed();
+        executor.execute(() -> {
+            delivery.disposition(state, settled);
+        });
+    }
+
+    void abort(OutgoingDelivery delivery, ClientTracker tracker) throws ClientException {
+        checkClosedOrFailed();
+        ClientFuture<Tracker> request = session().getFutureFactory().createFuture(new ClientSynchronization<Tracker>() {
+
+            @Override
+            public void onPendingSuccess(Tracker result) {
+                handleCreditStateUpdated(getProtonSender());
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+                handleCreditStateUpdated(getProtonSender());
+            }
+        });
+
+        executor.execute(() -> {
+            if (delivery.getTransferCount() == 0) {
+                delivery.abort();
+                request.complete(tracker);
+            } else {
+                ClientOutgoingEnvelope envelope = new ClientOutgoingEnvelope(this, delivery, delivery.getMessageFormat(), null, false, request).abort();
+                try {
+                    if (protonSender.isSendable() && (protonSender.current() == null || protonSender.current() == delivery)) {
+                        envelope.sendPayload(delivery.getState(), delivery.isSettled());
+                    } else {
+                        if (protonSender.current() == delivery) {
+                            addToHeadOfBlockedQueue(envelope);
+                        } else {
+                            addToTailOfBlockedQueue(envelope);
+                        }
+                    }
+                } catch (Exception error) {
+                    request.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+                }
+            }
+        });
+
+        session.request(this, request);
+    }
+
+    void complete(OutgoingDelivery delivery, ClientTracker tracker) throws ClientException {
+        checkClosedOrFailed();
+        ClientFuture<Tracker> request = session().getFutureFactory().createFuture(new ClientSynchronization<Tracker>() {
+
+            @Override
+            public void onPendingSuccess(Tracker result) {
+                handleCreditStateUpdated(getProtonSender());
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+                handleCreditStateUpdated(getProtonSender());
+            }
+        });
+
+        executor.execute(() -> {
+            ClientOutgoingEnvelope envelope = new ClientOutgoingEnvelope(this, delivery, delivery.getMessageFormat(), null, true, request);
+            try {
+                if (protonSender.isSendable() && (protonSender.current() == null || protonSender.current() == delivery)) {
+                    envelope.sendPayload(delivery.getState(), delivery.isSettled());
+                } else {
+                    if (protonSender.current() == delivery) {
+                        addToHeadOfBlockedQueue(envelope);
+                    } else {
+                        addToTailOfBlockedQueue(envelope);
+                    }
+                }
+            } catch (Exception error) {
+                request.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        session.request(this, request);
+    }
+
+    ClientSender open() {
+        protonSender.localOpenHandler(this::handleLocalOpen)
+                    .localCloseHandler(this::handleLocalCloseOrDetach)
+                    .localDetachHandler(this::handleLocalCloseOrDetach)
+                    .openHandler(this::handleRemoteOpen)
+                    .closeHandler(this::handleRemoteCloseOrDetach)
+                    .detachHandler(this::handleRemoteCloseOrDetach)
+                    .parentEndpointClosedHandler(this::handleParentEndpointClosed)
+                    .creditStateUpdateHandler(this::handleCreditStateUpdated)
+                    .engineShutdownHandler(this::handleEngineShutdown)
+                    .open();
+
+        return this;
+    }
+
+    void setFailureCause(ClientException failureCause) {
+        this.failureCause = failureCause;
+    }
+
+    org.apache.qpid.protonj2.engine.Sender getProtonSender() {
+        return protonSender;
+    }
+
+    ClientException getFailureCause() {
+        if (failureCause == null) {
+            return session.getFailureCause();
+        } else {
+            return failureCause;
+        }
+    }
+
+    String getId() {
+        return senderId;
+    }
+
+    boolean isClosed() {
+        return closed > 0;
+    }
+
+    boolean isAnonymous() {
+        return protonSender.<org.apache.qpid.protonj2.types.messaging.Target>getTarget().getAddress() == null;
+    }
+
+    boolean isDynamic() {
+        return protonSender.getTarget() != null && protonSender.<org.apache.qpid.protonj2.types.messaging.Target>getTarget().isDynamic();
+    }
+
+    boolean isSendingSettled() {
+        return sendsSettled;
+    }
+
+    //----- Handlers for proton receiver events
+
+    private void handleLocalOpen(org.apache.qpid.protonj2.engine.Sender sender) {
+        if (options.openTimeout() > 0) {
+            executor.schedule(() -> {
+                if (!openFuture.isDone()) {
+                    immediateLinkShutdown(new ClientOperationTimedOutException("Sender open timed out waiting for remote to respond"));
+                }
+            }, options.openTimeout(), TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void handleLocalCloseOrDetach(org.apache.qpid.protonj2.engine.Sender sender) {
+        // If not yet remotely closed we only wait for a remote close if the engine isn't
+        // already failed and we have successfully opened the sender without a timeout.
+        if (!sender.getEngine().isShutdown() && failureCause == null && sender.isRemotelyOpen()) {
+            final long timeout = options.closeTimeout();
+
+            if (timeout > 0) {
+                session.scheduleRequestTimeout(closeFuture, timeout, () ->
+                    new ClientOperationTimedOutException("Sender close timed out waiting for remote to respond"));
+            }
+        } else {
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleParentEndpointClosed(org.apache.qpid.protonj2.engine.Sender sender) {
+        // Don't react if engine was shutdown and parent closed as a result instead wait to get the
+        // shutdown notification and respond to that change.
+        if (sender.getEngine().isRunning()) {
+            final ClientException failureCause;
+
+            if (sender.getConnection().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(sender.getConnection().getRemoteCondition());
+            } else if (sender.getSession().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToSessionClosedException(sender.getSession().getRemoteCondition());
+            } else if (sender.getEngine().failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(sender.getEngine().failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientResourceRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleRemoteOpen(org.apache.qpid.protonj2.engine.Sender sender) {
+        // Check for deferred close pending and hold completion if so
+        if (sender.getRemoteTarget() != null) {
+            remoteSource = new ClientRemoteSource(sender.getRemoteSource());
+
+            if (sender.getRemoteTarget() != null) {
+                remoteTarget = new ClientRemoteTarget(sender.getRemoteTarget());
+            }
+
+            openFuture.complete(this);
+            LOG.trace("Sender opened successfully");
+        } else {
+            LOG.debug("Sender opened but remote signalled close is pending: ", sender);
+        }
+    }
+
+    private void handleRemoteCloseOrDetach(org.apache.qpid.protonj2.engine.Sender sender) {
+        if (sender.isLocallyOpen()) {
+            try {
+                senderRemotelyClosedHandler.accept(this);
+            } catch (Throwable ignore) {}
+
+            immediateLinkShutdown(ClientExceptionSupport.convertToLinkClosedException(
+                sender.getRemoteCondition(), "Sender remotely closed without explanation from the remote"));
+        } else {
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleCreditStateUpdated(org.apache.qpid.protonj2.engine.Sender sender) {
+        if (!blocked.isEmpty()) {
+            while (sender.isSendable() && !blocked.isEmpty()) {
+                ClientOutgoingEnvelope held = blocked.peek();
+                if (held.delivery() == protonSender.current()) {
+                    LOG.trace("Dispatching previously held send");
+                    try {
+                        // We don't currently allow a sender to define any outcome so we pass null for
+                        // now, however a transaction context will apply its TransactionalState outcome
+                        // and would wrap anything we passed in the future.
+                        session.getTransactionContext().send(held, null, isSendingSettled());
+                    } catch (Exception error) {
+                        held.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+                    } finally {
+                        blocked.poll();
+                    }
+                } else {
+                    break;
+                }
+            }
+        }
+
+        if (sender.isDraining() && sender.current() == null && blocked.isEmpty()) {
+            sender.drained();
+        }
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        if (!isDynamic() && !session.getConnection().getEngine().isShutdown()) {
+            protonSender.localCloseHandler(null);
+            protonSender.localDetachHandler(null);
+            protonSender.close();
+            if (protonSender.hasUnsettled()) {
+                failePendingUnsttledAndBlockedSends(
+                    new ClientConnectionRemotelyClosedException("Connection failed and send result is unknown"));
+            }
+            protonSender = ClientSenderBuilder.recreateSender(session, protonSender, options);
+            protonSender.setLinkedResource(this);
+
+            open();
+        } else {
+            final Connection connection = engine.connection();
+
+            final ClientException failureCause;
+
+            if (connection.getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(connection.getRemoteCondition());
+            } else if (engine.failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(engine.failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientConnectionRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    void handleAnonymousRelayNotSupported() {
+        if (isAnonymous() && protonSender.getState() == LinkState.IDLE) {
+            immediateLinkShutdown(new ClientUnsupportedOperationException("Anonymous relay support not available from this connection"));
+        }
+    }
+
+    //----- Private implementation details
+
+    private void waitForOpenToComplete() throws ClientException {
+        if (!openFuture.isComplete() || openFuture.isFailed()) {
+            try {
+                openFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                Thread.interrupted();
+                if (failureCause != null) {
+                    throw failureCause;
+                } else {
+                    throw ClientExceptionSupport.createNonFatalOrPassthrough(e.getCause());
+                }
+            }
+        }
+    }
+
+    protected final void addToTailOfBlockedQueue(ClientOutgoingEnvelope send) {
+        if (options.sendTimeout() > 0 && send.sendTimeout() == null) {
+            send.sendTimeout(executor.schedule(() -> {
+                send.failed(send.createSendTimedOutException());
+            }, options.sendTimeout(), TimeUnit.MILLISECONDS));
+        }
+
+        blocked.addLast(send);
+    }
+
+    protected final void addToHeadOfBlockedQueue(ClientOutgoingEnvelope send) {
+        if (options.sendTimeout() > 0 && send.sendTimeout() == null) {
+            send.sendTimeout(executor.schedule(() -> {
+                send.failed(send.createSendTimedOutException());
+            }, options.sendTimeout(), TimeUnit.MILLISECONDS));
+        }
+
+        blocked.addFirst(send);
+    }
+
+    protected Tracker sendMessage(AdvancedMessage<?> message, Map<String, Object> deliveryAnnotations, boolean waitForCredit) throws ClientException {
+        final ClientFuture<Tracker> operation = session.getFutureFactory().createFuture();
+        final ProtonBuffer buffer = message.encode(deliveryAnnotations);
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(operation)) {
+                try {
+                    final ClientOutgoingEnvelope envelope = new ClientOutgoingEnvelope(this, message.messageFormat(), buffer, operation);
+
+                    if (protonSender.isSendable() && protonSender.current() == null) {
+                        session.getTransactionContext().send(envelope, null, protonSender.getSenderSettleMode() == SenderSettleMode.SETTLED);
+                    } else if (waitForCredit) {
+                        addToTailOfBlockedQueue(envelope);
+                    } else {
+                        operation.complete(null);
+                    }
+                } catch (Exception error) {
+                    operation.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+                }
+            }
+        });
+
+        return session.request(this, operation);
+    }
+
+    protected Tracker createTracker(OutgoingDelivery delivery) {
+        return new ClientTracker(this, delivery);
+    }
+
+    protected Tracker createNoOpTracker() {
+        return new ClientNoOpTracker(this);
+    }
+
+    protected boolean notClosedOrFailed(ClientFuture<?> request) {
+        if (isClosed()) {
+            request.failed(new ClientIllegalStateException("The Sender was explicity closed", failureCause));
+            return false;
+        } else if (failureCause != null) {
+            request.failed(failureCause);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    protected void checkClosedOrFailed() throws ClientException {
+        if (isClosed()) {
+            throw new ClientIllegalStateException("The Sender was explicity closed", failureCause);
+        } else if (failureCause != null) {
+            throw failureCause;
+        }
+    }
+
+    private void immediateLinkShutdown(ClientException failureCause) {
+        if (this.failureCause == null) {
+            this.failureCause = failureCause;
+        }
+
+        try {
+            if (protonSender.isRemotelyDetached()) {
+                protonSender.detach();
+            } else {
+                protonSender.close();
+            }
+        } catch (Throwable ignore) {
+            // Ignore
+        } finally {
+            // If the parent of this sender is a stream session than this sender owns it
+            // and must close it when it closes itself to ensure that the resources are
+            // cleaned up on the remote for the session.
+            if (session instanceof ClientStreamSession) {
+                session.closeAsync();
+            }
+        }
+
+        if (failureCause != null) {
+            failePendingUnsttledAndBlockedSends(failureCause);
+        } else {
+            failePendingUnsttledAndBlockedSends(new ClientResourceRemotelyClosedException("The sender link has closed"));
+        }
+
+        if (failureCause != null) {
+            openFuture.failed(failureCause);
+        } else {
+            openFuture.complete(this);
+        }
+
+        closeFuture.complete(this);
+    }
+
+    private void failePendingUnsttledAndBlockedSends(ClientException cause) {
+        // Cancel all settlement futures for in-flight sends passing an appropriate error to the future
+        protonSender.unsettled().forEach((delivery) -> {
+            try {
+                final ClientTracker tracker = delivery.getLinkedResource();
+                tracker.settlementFuture().failed(cause);
+            } catch (Exception e) {
+            }
+        });
+
+        // Cancel all blocked sends passing an appropriate error to the future
+        blocked.removeIf((held) -> {
+            held.failed(cause);
+            return true;
+        });
+    }
+}
+
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSenderBuilder.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSenderBuilder.java
new file mode 100644
index 0000000..b1550af
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSenderBuilder.java
@@ -0,0 +1,243 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.SourceOptions;
+import org.apache.qpid.protonj2.client.StreamSenderOptions;
+import org.apache.qpid.protonj2.client.TargetOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.impl.ProtonDeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+
+/**
+ * Session owned builder of {@link Sender} objects.
+ */
+final class ClientSenderBuilder {
+
+    private final ClientSession session;
+    private final SessionOptions sessionOptions;
+    private final AtomicInteger senderCounter = new AtomicInteger();
+
+    private SenderOptions defaultSenderOptions;
+    private StreamSenderOptions defaultStreamSenderOptions;
+
+    public ClientSenderBuilder(ClientSession session) {
+        this.session = session;
+        this.sessionOptions = session.options();
+    }
+
+    public ClientSender sender(String address, SenderOptions senderOptions) throws ClientException {
+        final SenderOptions options = senderOptions != null ? senderOptions : getDefaultSenderOptions();
+        final String senderId = nextSenderId();
+        final Sender protonSender = createSender(session.getProtonSession(), address, options, senderId);
+
+        return new ClientSender(session, options, senderId, protonSender);
+    }
+
+    public ClientSender anonymousSender(SenderOptions senderOptions) throws ClientException {
+        final SenderOptions options = senderOptions != null ? senderOptions : getDefaultSenderOptions();
+        final String senderId = nextSenderId();
+        final Sender protonSender = createSender(session.getProtonSession(), null, options, senderId);
+
+        return new ClientSender(session, options, senderId, protonSender);
+    }
+
+    public ClientStreamSender streamSender(String address, StreamSenderOptions senderOptions) throws ClientException {
+        final StreamSenderOptions options = senderOptions != null ? senderOptions : getDefaultStreamSenderOptions();
+        final String senderId = nextSenderId();
+        final Sender protonSender = createSender(session.getProtonSession(), address, options, senderId);
+
+        return new ClientStreamSender(session, options, senderId, protonSender);
+    }
+
+    private static Sender createSender(Session protonSession, String address, SenderOptions options, String senderId) {
+        final String linkName;
+
+        if (options.linkName() != null) {
+            linkName = options.linkName();
+        } else {
+            linkName = "sender-" + senderId;
+        }
+
+        final Sender protonSender = protonSession.sender(linkName);
+
+        switch (options.deliveryMode()) {
+            case AT_MOST_ONCE:
+                protonSender.setSenderSettleMode(SenderSettleMode.SETTLED);
+                protonSender.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+                break;
+            case AT_LEAST_ONCE:
+                protonSender.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+                protonSender.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+                break;
+        }
+
+        protonSender.setOfferedCapabilities(ClientConversionSupport.toSymbolArray(options.offeredCapabilities()));
+        protonSender.setDesiredCapabilities(ClientConversionSupport.toSymbolArray(options.desiredCapabilities()));
+        protonSender.setProperties(ClientConversionSupport.toSymbolKeyedMap(options.properties()));
+        protonSender.setTarget(createTarget(address, options));
+        protonSender.setSource(createSource(senderId, options));
+
+        // Use a tag generator that will reuse old tags.  Later we might make this configurable.
+        if (protonSender.getSenderSettleMode() == SenderSettleMode.SETTLED) {
+            protonSender.setDeliveryTagGenerator(ProtonDeliveryTagGenerator.BUILTIN.EMPTY.createGenerator());
+        } else {
+            protonSender.setDeliveryTagGenerator(ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator());
+        }
+
+        return protonSender;
+    }
+
+    private static Source createSource(String address, SenderOptions options) {
+        final SourceOptions sourceOptions = options.sourceOptions();
+        final Source source = new Source();
+
+        source.setAddress(address);
+        source.setOutcomes(ClientConversionSupport.outcomesToSymbols(sourceOptions.outcomes()));
+        source.setDefaultOutcome((Outcome) ClientDeliveryState.asProtonType(sourceOptions.defaultOutcome()));
+        source.setCapabilities(ClientConversionSupport.toSymbolArray(sourceOptions.capabilities()));
+
+        if (sourceOptions.timeout() >= 0) {
+            source.setTimeout(UnsignedInteger.valueOf(sourceOptions.timeout()));
+        }
+        if (sourceOptions.durabilityMode() != null) {
+            source.setDurable(ClientConversionSupport.asProtonType(sourceOptions.durabilityMode()));
+        } else {
+            source.setDurable(TerminusDurability.NONE);
+        }
+        if (sourceOptions.expiryPolicy() != null) {
+            source.setExpiryPolicy(ClientConversionSupport.asProtonType(sourceOptions.expiryPolicy()));
+        } else {
+            source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH);
+        }
+        if (sourceOptions.distributionMode() != null) {
+            source.setDistributionMode(ClientConversionSupport.asProtonType(sourceOptions.distributionMode()));
+        }
+        if (sourceOptions.timeout() >= 0) {
+            source.setTimeout(UnsignedInteger.valueOf(sourceOptions.timeout()));
+        }
+        if (sourceOptions.filters() != null) {
+            source.setFilter(ClientConversionSupport.toSymbolKeyedMap(sourceOptions.filters()));
+        }
+
+        return source;
+    }
+
+    private static Target createTarget(String address, SenderOptions options) {
+        final TargetOptions targetOptions = options.targetOptions();
+        final Target target = new Target();
+
+        target.setAddress(address);
+        target.setCapabilities(ClientConversionSupport.toSymbolArray(targetOptions.capabilities()));
+
+        if (targetOptions.durabilityMode() != null) {
+            target.setDurable(ClientConversionSupport.asProtonType(targetOptions.durabilityMode()));
+        }
+        if (targetOptions.expiryPolicy() != null) {
+            target.setExpiryPolicy(ClientConversionSupport.asProtonType(targetOptions.expiryPolicy()));
+        }
+        if (targetOptions.timeout() >= 0) {
+            target.setTimeout(UnsignedInteger.valueOf(targetOptions.timeout()));
+        }
+
+        return target;
+    }
+
+    public static Sender recreateSender(ClientSession session, Sender previousSender, SenderOptions options) {
+        final Sender protonSender = session.getProtonSession().sender(previousSender.getName());
+
+        protonSender.setSource(previousSender.getSource());
+        if (previousSender.getTarget() instanceof Coordinator) {
+            protonSender.setTarget((Coordinator) previousSender.getTarget());
+        } else {
+            protonSender.setTarget((Target) previousSender.getTarget());
+        }
+
+        protonSender.setDeliveryTagGenerator(previousSender.getDeliveryTagGenerator());
+        protonSender.setSenderSettleMode(previousSender.getSenderSettleMode());
+        protonSender.setReceiverSettleMode(previousSender.getReceiverSettleMode());
+        protonSender.setOfferedCapabilities(ClientConversionSupport.toSymbolArray(options.offeredCapabilities()));
+        protonSender.setDesiredCapabilities(ClientConversionSupport.toSymbolArray(options.desiredCapabilities()));
+        protonSender.setProperties(ClientConversionSupport.toSymbolKeyedMap(options.properties()));
+
+        return protonSender;
+    }
+
+    private String nextSenderId() {
+        return session.id() + ":" + senderCounter.incrementAndGet();
+    }
+
+    /*
+     * Sender options used when none specified by the caller creating a new sender.
+     */
+    private SenderOptions getDefaultSenderOptions() {
+        SenderOptions senderOptions = defaultSenderOptions;
+        if (senderOptions == null) {
+            synchronized (this) {
+                senderOptions = defaultSenderOptions;
+                if (senderOptions == null) {
+                    senderOptions = new SenderOptions();
+                    senderOptions.openTimeout(sessionOptions.openTimeout());
+                    senderOptions.closeTimeout(sessionOptions.closeTimeout());
+                    senderOptions.requestTimeout(sessionOptions.requestTimeout());
+                    senderOptions.sendTimeout(sessionOptions.sendTimeout());
+                }
+
+                defaultSenderOptions = senderOptions;
+            }
+        }
+
+        return senderOptions;
+    }
+
+    /*
+     * Stream Sender options used when none specified by the caller creating a new sender.
+     */
+    private StreamSenderOptions getDefaultStreamSenderOptions() {
+        StreamSenderOptions senderOptions = defaultStreamSenderOptions;
+        if (senderOptions == null) {
+            synchronized (this) {
+                senderOptions = defaultStreamSenderOptions;
+                if (senderOptions == null) {
+                    senderOptions = new StreamSenderOptions();
+                    senderOptions.openTimeout(sessionOptions.openTimeout());
+                    senderOptions.closeTimeout(sessionOptions.closeTimeout());
+                    senderOptions.requestTimeout(sessionOptions.requestTimeout());
+                    senderOptions.sendTimeout(sessionOptions.sendTimeout());
+                }
+
+                defaultStreamSenderOptions = senderOptions;
+            }
+        }
+
+        return senderOptions;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSession.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSession.java
new file mode 100644
index 0000000..5f86def
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSession.java
@@ -0,0 +1,591 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.function.Supplier;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.StreamReceiverOptions;
+import org.apache.qpid.protonj2.client.StreamSenderOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.futures.AsyncResult;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.client.futures.ClientFutureFactory;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Client implementation of the Session API.
+ */
+public class ClientSession implements Session {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientSession.class);
+
+    private static final long INFINITE = -1;
+
+    private static final AtomicIntegerFieldUpdater<ClientSession> CLOSED_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(ClientSession.class, "closed");
+    private static final ClientNoOpTransactionContext NO_OP_TXN_CONTEXT = new ClientNoOpTransactionContext();
+
+    private final ClientFuture<Session> openFuture;
+    private final ClientFuture<Session> closeFuture;
+
+    private final SessionOptions options;
+    private final ClientConnection connection;
+    private final ScheduledExecutorService serializer;
+    private final String sessionId;
+    private final ClientSenderBuilder senderBuilder;
+    private final ClientReceiverBuilder receiverBuilder;
+
+    private volatile int closed;
+    private volatile ClientException failureCause;
+    private ClientTransactionContext txnContext = NO_OP_TXN_CONTEXT;
+
+    private org.apache.qpid.protonj2.engine.Session protonSession;
+
+    public ClientSession(ClientConnection connection, SessionOptions options, String sessionId, org.apache.qpid.protonj2.engine.Session session) {
+        this.options = new SessionOptions(options);
+        this.connection = connection;
+        this.protonSession = session.setLinkedResource(this);
+        this.sessionId = sessionId;
+        this.serializer = connection.getScheduler();
+        this.openFuture = connection.getFutureFactory().createFuture();
+        this.closeFuture = connection.getFutureFactory().createFuture();
+        this.senderBuilder = new ClientSenderBuilder(this);
+        this.receiverBuilder = new ClientReceiverBuilder(this);
+
+        configureSession(protonSession);
+    }
+
+    @Override
+    public ClientInstance client() {
+        return connection.client();
+    }
+
+    @Override
+    public ClientConnection connection() {
+        return connection;
+    }
+
+    @Override
+    public Future<Session> openFuture() {
+        return openFuture;
+    }
+
+    @Override
+    public void close() {
+        try {
+            doClose(null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void close(ErrorCondition error) {
+        try {
+            doClose(error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public Future<Session> closeAsync() {
+        return doClose(null);
+    }
+
+    @Override
+    public Future<Session> closeAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "Supplied error condition cannot be null");
+        return doClose(error);
+    }
+
+    private Future<Session> doClose(ErrorCondition error) {
+        if (CLOSED_UPDATER.compareAndSet(this, 0, 1)) {
+            // Already closed by failure or shutdown so no need to
+            if (!closeFuture.isDone()) {
+                serializer.execute(() -> {
+                    if (protonSession.isLocallyOpen()) {
+                        try {
+                            protonSession.setCondition(ClientErrorCondition.asProtonErrorCondition(error));
+                            protonSession.close();
+                        } catch (Throwable ignore) {
+                            // Allow engine error handler to deal with this
+                        }
+                    }
+                });
+            }
+        }
+
+        return closeFuture;
+    }
+
+    @Override
+    public Receiver openReceiver(String address) throws ClientException {
+        return openReceiver(address, null);
+    }
+
+    @Override
+    public Receiver openReceiver(String address, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a receiver with a null address");
+        final ClientFuture<Receiver> createReceiver = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createReceiver.complete(internalOpenReceiver(address, receiverOptions));
+            } catch (Throwable error) {
+                createReceiver.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, createReceiver);
+    }
+
+    @Override
+    public Receiver openDurableReceiver(String address, String subscriptionName) throws ClientException {
+        return openDurableReceiver(address, subscriptionName, null);
+    }
+
+    @Override
+    public Receiver openDurableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a receiver with a null address");
+        final ClientFuture<Receiver> createReceiver = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createReceiver.complete(internalOpenDurableReceiver(address, subscriptionName, receiverOptions));
+            } catch (Throwable error) {
+                createReceiver.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, createReceiver);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver() throws ClientException {
+        return openDynamicReceiver(null, null);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties) throws ClientException {
+        return openDynamicReceiver(dynamicNodeProperties, null);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(ReceiverOptions receiverOptions) throws ClientException {
+        return openDynamicReceiver(null, receiverOptions);
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Receiver> createReceiver = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createReceiver.complete(internalOpenDynamicReceiver(dynamicNodeProperties, receiverOptions));
+            } catch (Throwable error) {
+                createReceiver.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, createReceiver);
+    }
+
+    @Override
+    public Sender openSender(String address) throws ClientException {
+        return openSender(address, null);
+    }
+
+    @Override
+    public Sender openSender(String address, SenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        Objects.requireNonNull(address, "Cannot create a sender with a null address");
+        final ClientFuture<Sender> createSender = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createSender.complete(internalOpenSender(address, senderOptions));
+            } catch (Throwable error) {
+                createSender.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, createSender);
+    }
+
+    @Override
+    public Sender openAnonymousSender() throws ClientException {
+        return openAnonymousSender(null);
+    }
+
+    @Override
+    public Sender openAnonymousSender(SenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Sender> createSender = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                createSender.complete(internalOpenAnonymousSender(senderOptions));
+            } catch (Throwable error) {
+                createSender.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, createSender);
+    }
+
+    @Override
+    public Map<String, Object> properties() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringKeyedMap(protonSession.getRemoteProperties());
+    }
+
+    @Override
+    public String[] offeredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonSession.getRemoteOfferedCapabilities());
+    }
+
+    @Override
+    public String[] desiredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonSession.getRemoteDesiredCapabilities());
+    }
+
+    //----- Transaction state management
+
+    @Override
+    public Session beginTransaction() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Session> beginFuture = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                if (txnContext == NO_OP_TXN_CONTEXT) {
+                    txnContext = new ClientLocalTransactionContext(this);
+                }
+                txnContext.begin(beginFuture);
+            } catch (Throwable error) {
+                beginFuture.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, beginFuture);
+    }
+
+    @Override
+    public Session commitTransaction() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Session> commitFuture = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                txnContext.commit(commitFuture, false);
+            } catch (Throwable error) {
+                commitFuture.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, commitFuture);
+    }
+
+    @Override
+    public Session rollbackTransaction() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Session> rollbackFuture = getFutureFactory().createFuture();
+
+        serializer.execute(() -> {
+            try {
+                checkClosedOrFailed();
+                txnContext.rollback(rollbackFuture, false);
+            } catch (Throwable error) {
+                rollbackFuture.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+            }
+        });
+
+        return connection.request(this, rollbackFuture);
+    }
+
+    //----- Internal resource open APIs expected to be called from the connection event loop
+
+    ClientReceiver internalOpenReceiver(String address, ReceiverOptions receiverOptions) throws ClientException {
+        return receiverBuilder.receiver(address, receiverOptions).open();
+    }
+
+    ClientStreamReceiver internalOpenStreamReceiver(String address, StreamReceiverOptions receiverOptions) throws ClientException {
+        return receiverBuilder.streamReceiver(address, receiverOptions).open();
+    }
+
+    ClientReceiver internalOpenDurableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) throws ClientException {
+        return receiverBuilder.durableReceiver(address, subscriptionName, receiverOptions).open();
+    }
+
+    ClientReceiver internalOpenDynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException {
+        return receiverBuilder.dynamicReceiver(dynamicNodeProperties, receiverOptions).open();
+    }
+
+    ClientSender internalOpenSender(String address, SenderOptions senderOptions) throws ClientException {
+        return senderBuilder.sender(address, senderOptions).open();
+    }
+
+    ClientSender internalOpenAnonymousSender(SenderOptions senderOptions) throws ClientException {
+        // When the connection is opened we are ok to check that the anonymous relay is supported
+        // and open the sender if so, otherwise we need to wait.
+        if (connection.openFuture().isDone()) {
+            connection.checkAnonymousRelaySupported();
+            return senderBuilder.anonymousSender(senderOptions).open();
+        } else {
+            return senderBuilder.anonymousSender(senderOptions);
+        }
+    }
+
+    ClientStreamSender internalOpenStreamSender(String address, StreamSenderOptions senderOptions) throws ClientException {
+        return senderBuilder.streamSender(address, senderOptions).open();
+    }
+
+    //----- Internal API accessible for use within the package
+
+    ClientSession open() {
+        protonSession.localOpenHandler(this::handleLocalOpen)
+                     .localCloseHandler(this::handleLocalClose)
+                     .openHandler(this::handleRemoteOpen)
+                     .closeHandler(this::handleRemoteClose)
+                     .engineShutdownHandler(this::handleEngineShutdown);
+
+        try {
+            protonSession.open();
+        } catch (Throwable error) {
+            // Connection is responding to all engine failed errors
+        }
+
+        return this;
+    }
+
+    ScheduledExecutorService getScheduler() {
+        return serializer;
+    }
+
+    ClientFutureFactory getFutureFactory() {
+        return connection.getFutureFactory();
+    }
+
+    ClientException getFailureCause() {
+        return failureCause;
+    }
+
+    boolean isClosed() {
+        return closed > 0;
+    }
+
+    ScheduledFuture<?> scheduleRequestTimeout(final AsyncResult<?> request, long timeout, Supplier<ClientException> errorSupplier) {
+        if (timeout != INFINITE) {
+            return serializer.schedule(() -> request.failed(errorSupplier.get()), timeout, TimeUnit.MILLISECONDS);
+        } else {
+            return null;
+        }
+    }
+
+    <T> T request(Object requestor, ClientFuture<T> request) throws ClientException {
+        return connection.request(requestor, request);
+    }
+
+    String id() {
+        return sessionId;
+    }
+
+    SessionOptions options() {
+        return options;
+    }
+
+    org.apache.qpid.protonj2.engine.Session getProtonSession() {
+        return protonSession;
+    }
+
+    ClientTransactionContext getTransactionContext() {
+        return txnContext;
+    }
+
+    ClientConnection getConnection() {
+        return connection;
+    }
+
+    //----- Private implementation methods
+
+    private org.apache.qpid.protonj2.engine.Session configureSession(org.apache.qpid.protonj2.engine.Session protonSession) {
+        protonSession.setLinkedResource(this);
+        protonSession.setOfferedCapabilities(ClientConversionSupport.toSymbolArray(options.offeredCapabilities()));
+        protonSession.setDesiredCapabilities(ClientConversionSupport.toSymbolArray(options.desiredCapabilities()));
+        protonSession.setProperties(ClientConversionSupport.toSymbolKeyedMap(options.properties()));
+
+        return protonSession;
+    }
+
+    protected void checkClosedOrFailed() throws ClientException {
+        if (isClosed()) {
+            throw new ClientIllegalStateException("The Session was explicity closed", failureCause);
+        } else if (failureCause != null) {
+            throw failureCause;
+        }
+    }
+
+    private void waitForOpenToComplete() throws ClientException {
+        if (!openFuture.isComplete() || openFuture.isFailed()) {
+            try {
+                openFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                Thread.interrupted();
+                if (failureCause != null) {
+                    throw failureCause;
+                } else {
+                    throw ClientExceptionSupport.createNonFatalOrPassthrough(e.getCause());
+                }
+            }
+        }
+    }
+
+    //----- Handle Events from the Proton Session
+
+    private void handleLocalOpen(org.apache.qpid.protonj2.engine.Session session) {
+        if (options.openTimeout() > 0) {
+            serializer.schedule(() -> {
+                if (!openFuture.isDone()) {
+                    immediateSessionShutdown(new ClientOperationTimedOutException("Session open timed out waiting for remote to respond"));
+                }
+            }, options.openTimeout(), TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void handleLocalClose(org.apache.qpid.protonj2.engine.Session session) {
+        // If not yet remotely closed we only wait for a remote close if the engine isn't
+        // already failed and we have successfully opened the session without a timeout.
+        if (session.isRemotelyOpen() && failureCause == null && !session.getEngine().isShutdown()) {
+            final long timeout = options.closeTimeout();
+
+            if (timeout > 0) {
+                scheduleRequestTimeout(closeFuture, timeout, () ->
+                    new ClientOperationTimedOutException("Session close timed out waiting for remote to respond"));
+            }
+        } else {
+            immediateSessionShutdown(failureCause);
+        }
+    }
+
+    private void handleRemoteOpen(org.apache.qpid.protonj2.engine.Session session) {
+        openFuture.complete(this);
+        LOG.trace("Session:{} opened successfully.", id());
+
+        session.senders().forEach(sender -> {
+            if (!sender.isLocallyOpen()) {
+                ClientSender clientSender = sender.getLinkedResource();
+                if (connection.getCapabilities().anonymousRelaySupported()) {
+                    clientSender.open();
+                } else {
+                    clientSender.handleAnonymousRelayNotSupported();
+                }
+            }
+        });
+    }
+
+    private void handleRemoteClose(org.apache.qpid.protonj2.engine.Session session) {
+        if (session.isLocallyOpen()) {
+            immediateSessionShutdown(ClientExceptionSupport.convertToSessionClosedException(session.getRemoteCondition()));
+        } else {
+            immediateSessionShutdown(failureCause);
+        }
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        // If the connection has an engine that is running then it is going to attempt
+        // reconnection and we want to recover by creating a new Session that will be
+        // opened once the remote has been recovered.
+        if (!connection.getEngine().isShutdown()) {
+            // No local close processing needed but we should try and let the session
+            // clean up any resources it can by closing it.
+            protonSession.localCloseHandler(null);
+            protonSession.close();
+            protonSession = configureSession(ClientSessionBuilder.recreateSession(connection, protonSession, options));
+
+            open();
+        } else {
+            final Connection connection = engine.connection();
+
+            final ClientException failureCause;
+
+            if (connection.getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(connection.getRemoteCondition());
+            } else if (engine.failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(engine.failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientConnectionRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateSessionShutdown(failureCause);
+        }
+    }
+
+    private void immediateSessionShutdown(ClientException failureCause) {
+        if (this.failureCause == null) {
+            this.failureCause = failureCause;
+        }
+
+        try {
+            protonSession.close();
+        } catch (Exception ignore) {
+        }
+
+        if (failureCause != null) {
+            openFuture.failed(failureCause);
+        } else {
+            openFuture.complete(this);
+        }
+
+        closeFuture.complete(this);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSessionBuilder.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSessionBuilder.java
new file mode 100644
index 0000000..743f3d6
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientSessionBuilder.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Session;
+
+final class ClientSessionBuilder {
+
+    private final AtomicInteger sessionCounter = new AtomicInteger();
+    private final ClientConnection connection;
+    private final ConnectionOptions connectionOptions;
+
+    private SessionOptions defaultSessionOptions;
+
+    public ClientSessionBuilder(ClientConnection connection) {
+        this.connection = connection;
+        this.connectionOptions = connection.getOptions();
+    }
+
+    public ClientSession session(SessionOptions sessionOptions) throws ClientException {
+        final SessionOptions options = sessionOptions != null ? sessionOptions : getDefaultSessionOptions();
+        final String sessionId = nextSessionId();
+        final Session protonSession = createSession(connection.getProtonConnection(), options);
+
+        return new ClientSession(connection, options, sessionId, protonSession);
+    }
+
+    public ClientStreamSession streamSession(SessionOptions sessionOptions) throws ClientException {
+        final SessionOptions options = sessionOptions != null ? sessionOptions : getDefaultSessionOptions();
+        final String sessionId = nextSessionId();
+        final Session protonSession = createSession(connection.getProtonConnection(), options);
+
+        return new ClientStreamSession(connection, options, sessionId, protonSession);
+    }
+
+    private static Session createSession(Connection connection, SessionOptions options) {
+        return connection.session().setIncomingCapacity(options.incomingCapacity()).setOutgoingCapacity(options.outgoingCapacity());
+    }
+
+    public static Session recreateSession(ClientConnection connection, Session previousSession, SessionOptions options) {
+        return connection.getProtonConnection().session().setIncomingCapacity(options.incomingCapacity()).setOutgoingCapacity(options.outgoingCapacity());
+    }
+
+    /*
+     * Session options used when none specified by the caller creating a new session.
+     */
+    public SessionOptions getDefaultSessionOptions() {
+        SessionOptions sessionOptions = defaultSessionOptions;
+        if (sessionOptions == null) {
+            synchronized (this) {
+                sessionOptions = defaultSessionOptions;
+                if (sessionOptions == null) {
+                    sessionOptions = new SessionOptions();
+                    sessionOptions.openTimeout(connectionOptions.openTimeout());
+                    sessionOptions.closeTimeout(connectionOptions.closeTimeout());
+                    sessionOptions.requestTimeout(connectionOptions.requestTimeout());
+                    sessionOptions.sendTimeout(connectionOptions.sendTimeout());
+                    sessionOptions.drainTimeout(connectionOptions.drainTimeout());
+                }
+
+                defaultSessionOptions = sessionOptions;
+            }
+        }
+
+        return sessionOptions;
+    }
+
+    String nextSessionId() {
+        return connection.getId() + ":" + sessionCounter.incrementAndGet();
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamDelivery.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamDelivery.java
new file mode 100644
index 0000000..d3fa6e7
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamDelivery.java
@@ -0,0 +1,489 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.buffer.ProtonCompositeBuffer;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.StreamDelivery;
+import org.apache.qpid.protonj2.client.exceptions.ClientDeliveryAbortedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientResourceRemotelyClosedException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ClientStreamDelivery implements StreamDelivery {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientStreamDelivery.class);
+
+    private final ClientStreamReceiver receiver;
+    private final IncomingDelivery protonDelivery;
+
+    private ClientStreamReceiverMessage message;
+    private RawDeliveryInputStream rawInputStream;
+
+    public ClientStreamDelivery(ClientStreamReceiver receiver, IncomingDelivery protonDelivery) {
+        this.receiver = receiver;
+        this.protonDelivery = protonDelivery.setLinkedResource(this);
+
+        // Capture inbound events and route to an active stream or message
+        protonDelivery.deliveryReadHandler(this::handleDeliveryRead)
+                      .deliveryAbortedHandler(this::handleDeliveryAborted);
+    }
+
+    IncomingDelivery getProtonDelivery() {
+        return protonDelivery;
+    }
+
+    @Override
+    public ClientStreamReceiver receiver() {
+        return receiver;
+    }
+
+    @Override
+    public boolean aborted() {
+        return protonDelivery.isAborted();
+    }
+
+    @Override
+    public boolean completed() {
+        return !protonDelivery.isPartial();
+    }
+
+    @Override
+    public int messageFormat() {
+        return protonDelivery.getMessageFormat();
+    }
+
+    @Override
+    public ClientStreamReceiverMessage message() throws ClientException {
+        if (rawInputStream != null && message == null) {
+            throw new ClientIllegalStateException("Cannot access Delivery Message API after requesting an InputStream");
+        }
+
+        if (message == null) {
+            message = new ClientStreamReceiverMessage(receiver, this, rawInputStream = new RawDeliveryInputStream());
+        }
+
+        return message;
+    }
+
+    @Override
+    public Map<String, Object> annotations() throws ClientException {
+        if (rawInputStream != null && message == null) {
+            throw new ClientIllegalStateException("Cannot access Delivery Annotations API after requesting an InputStream");
+        }
+
+        return StringUtils.toStringKeyedMap(message().deliveryAnnotations() != null ? message().deliveryAnnotations().getValue() : null);
+    }
+
+    @Override
+    public InputStream rawInputStream() throws ClientException {
+        if (message != null) {
+            throw new ClientIllegalStateException("Cannot access Delivery InputStream API after requesting an Message");
+        }
+
+        if (rawInputStream == null) {
+            rawInputStream = new RawDeliveryInputStream();
+        }
+
+        return rawInputStream;
+    }
+
+    @Override
+    public StreamDelivery accept() throws ClientException {
+        receiver.disposition(protonDelivery, Accepted.getInstance(), true);
+        return this;
+    }
+
+    @Override
+    public StreamDelivery release() throws ClientException {
+        receiver.disposition(protonDelivery, Released.getInstance(), true);
+        return this;
+    }
+
+    @Override
+    public StreamDelivery reject(String condition, String description) throws ClientException {
+        receiver.disposition(protonDelivery, new Rejected().setError(new ErrorCondition(condition, description)), true);
+        return this;
+    }
+
+    @Override
+    public StreamDelivery modified(boolean deliveryFailed, boolean undeliverableHere) throws ClientException {
+        receiver.disposition(protonDelivery, new Modified().setDeliveryFailed(deliveryFailed).setUndeliverableHere(undeliverableHere), true);
+        return this;
+    }
+
+    @Override
+    public StreamDelivery disposition(DeliveryState state, boolean settle) throws ClientException {
+        receiver.disposition(protonDelivery, ClientDeliveryState.asProtonType(state), settle);
+        return this;
+    }
+
+    @Override
+    public StreamDelivery settle() throws ClientException {
+        receiver.disposition(protonDelivery, null, true);
+        return this;
+    }
+
+    @Override
+    public DeliveryState state() {
+        return ClientDeliveryState.fromProtonType(protonDelivery.getState());
+    }
+
+    @Override
+    public boolean settled() {
+        return protonDelivery.isSettled();
+    }
+
+    @Override
+    public DeliveryState remoteState() {
+        return ClientDeliveryState.fromProtonType(protonDelivery.getRemoteState());
+    }
+
+    @Override
+    public boolean remoteSettled() {
+        return protonDelivery.isRemotelySettled();
+    }
+
+    //----- Event Handlers for Delivery updates
+
+    void handleDeliveryRead(IncomingDelivery delivery) {
+        if (rawInputStream != null) {
+            rawInputStream.handleDeliveryRead(delivery);
+        }
+    }
+
+    void handleDeliveryAborted(IncomingDelivery delivery) {
+        if (rawInputStream != null) {
+            rawInputStream.handleDeliveryAborted(delivery);
+        }
+    }
+
+    void handleReceiverClosed(ClientStreamReceiver receiver) {
+        if (rawInputStream != null) {
+            rawInputStream.handleReceiverClosed(receiver);
+        }
+    }
+
+    //----- Raw InputStream Implementation
+
+    private class RawDeliveryInputStream extends InputStream {
+
+        private final int INVALID_MARK = -1;
+
+        private final ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        private final ScheduledExecutorService executor = receiver.session().getScheduler();
+
+        private ClientFuture<Integer> readRequest;
+
+        private AtomicBoolean closed = new AtomicBoolean();
+        private int markIndex = INVALID_MARK;
+        private int markLimit;
+
+        @Override
+        public void close() throws IOException {
+            markLimit = 0;
+            markIndex = INVALID_MARK;
+
+            if (closed.compareAndSet(false, true)) {
+                final ClientFuture<Void> closed = receiver.session().getFutureFactory().createFuture();
+
+                try {
+                    executor.execute(() -> {
+                        autoAcceptDeliveryIfNecessary();
+
+                        // If the deliver wasn't fully read either because there are remaining
+                        // bytes locally we need to discard those to aid in retention avoidance.
+                        // and to potentially open the session window to allow for fully reading
+                        // and discarding any inbound bytes that remain.
+                        try {
+                            protonDelivery.readAll();
+                        } catch (EngineFailedException efe) {
+                            // Ignore as engine is down and we cannot read any more
+                        }
+
+                        // Clear anything that wasn't yet read and then clear any pending read request as EOF
+                        buffer.setIndex(buffer.capacity(), buffer.capacity());
+                        buffer.reclaimRead();
+
+                        if (readRequest != null) {
+                            readRequest.complete(-1);
+                            readRequest = null;
+                        }
+
+                        closed.complete(null);
+                    });
+
+                    receiver.session().request(receiver, closed);
+                } catch (Exception error) {
+                    LOG.debug("Ignoring error on RawInputStream close: ", error);
+                } finally {
+                    super.close();
+                }
+            }
+        }
+
+        @Override
+        public boolean markSupported() {
+            return true;
+        }
+
+        @Override
+        public synchronized void mark(int readlimit) {
+            markIndex = buffer.getReadIndex();
+            markLimit = readlimit;
+        }
+
+        @Override
+        public synchronized void reset() throws IOException {
+            if (markIndex != INVALID_MARK) {
+                buffer.setReadIndex(markIndex);
+
+                markIndex = INVALID_MARK;
+                markLimit = 0;
+            }
+        }
+
+        @Override
+        public int available() throws IOException {
+            checkStreamStateIsValid();
+
+            // Check for any bytes in the delivery that haven't been moved to the read buffer yet
+            if (buffer.isReadable()) {
+                return buffer.getReadableBytes();
+            } else {
+                final ClientFuture<Integer> request = receiver.session().getFutureFactory().createFuture();
+
+                try {
+                    executor.execute(() -> {
+                        if (protonDelivery.available() > 0) {
+                            buffer.append(protonDelivery.readAll());
+                        }
+
+                        request.complete(buffer.getReadableBytes());
+                    });
+
+                    return receiver.session().request(receiver, request);
+                } catch (Exception e) {
+                    throw new IOException("Error reading requested data", e);
+                }
+            }
+        }
+
+        @Override
+        public int read() throws IOException {
+            checkStreamStateIsValid();
+
+            int result = -1;
+
+            while (true) {
+                if (buffer.isReadable()) {
+                    result = buffer.readByte() & 0xff;
+                    tryReleaseReadBuffers();
+                    break;
+                } else if (requestMoreData() < 0) {
+                    break;
+                }
+            }
+
+            return result;
+        }
+
+        @Override
+        public int read(byte target[], int offset, int length) throws IOException {
+            checkStreamStateIsValid();
+
+            Objects.checkFromIndexSize(offset, length, target.length);
+
+            int remaining = length;
+            int bytesRead = 0;
+
+            if (length <= 0) {
+                return 0;
+            }
+
+            while (remaining > 0) {
+                if (buffer.isReadable()) {
+                    if (buffer.getReadableBytes() < remaining) {
+                        final int readTarget = buffer.getReadableBytes();
+                        buffer.readBytes(target, offset + bytesRead, buffer.getReadableBytes());
+                        bytesRead += readTarget;
+                        remaining -= readTarget;
+                    } else {
+                        buffer.readBytes(target, offset + bytesRead, remaining);
+                        bytesRead += remaining;
+                        remaining = 0;
+                    }
+
+                    tryReleaseReadBuffers();
+                } else if (requestMoreData() < 0) {
+                    return bytesRead > 0 ? bytesRead : -1;
+                }
+            }
+
+            return bytesRead;
+        }
+
+        @Override
+        public long skip(long amount) throws IOException {
+            checkStreamStateIsValid();
+
+            long remaining = amount;
+
+            if (amount <= 0) {
+                return 0;
+            }
+
+            while (remaining > 0) {
+                if (buffer.isReadable()) {
+                    if (buffer.getReadableBytes() < remaining) {
+                        remaining -= buffer.getReadableBytes();
+                        buffer.skipBytes(buffer.getReadableBytes());
+                    } else {
+                        buffer.skipBytes((int) remaining);
+                        remaining = 0;
+                    }
+
+                    tryReleaseReadBuffers();
+                } else if (requestMoreData() < 0) {
+                    break;
+                }
+            }
+
+            return amount - remaining;
+        }
+
+        @Override
+        public long transferTo(OutputStream target) throws IOException {
+            checkStreamStateIsValid();
+            // TODO: Implement efficient read and forward without intermediate copies
+            //       from the currently available buffer to the output stream.
+            return super.transferTo(target);
+        }
+
+        private void tryReleaseReadBuffers() {
+            if (buffer.getReadIndex() - markIndex > markLimit) {
+                markIndex = INVALID_MARK;
+                markLimit = 0;
+                buffer.reclaimRead();
+            }
+        }
+
+        private void handleDeliveryRead(IncomingDelivery delivery) {
+            if (closed.get()) {
+                // Clear any pending data to expand session window if not yet complete
+                delivery.readAll();
+            } else {
+                // An input stream is awaiting some more incoming bytes, check to see if
+                // the delivery had a non-empty transfer frame and provide them.
+                if (readRequest != null) {
+                    if (delivery.available() > 0) {
+                        buffer.append(protonDelivery.readAll());
+                        readRequest.complete(buffer.getReadableBytes());
+                    } else if (!delivery.isPartial()) {
+                        autoAcceptDeliveryIfNecessary();
+                        readRequest.complete(-1);
+                    }
+
+                    readRequest = null;
+                }
+            }
+        }
+
+        private void handleDeliveryAborted(IncomingDelivery delivery) {
+            if (readRequest != null) {
+                readRequest.failed(new ClientDeliveryAbortedException("The remote sender has aborted this delivery"));
+            }
+
+            delivery.settle();
+        }
+
+        private void handleReceiverClosed(ClientStreamReceiver receiver) {
+            if (readRequest != null) {
+                readRequest.failed(new ClientResourceRemotelyClosedException("The receliver link has been remotely closed."));
+            }
+        }
+
+        private int requestMoreData() throws IOException {
+            final ClientFuture<Integer> request = receiver.session().getFutureFactory().createFuture();
+
+            try {
+                executor.execute(() -> {
+                    if (protonDelivery.getLink().isLocallyClosedOrDetached()) {
+                        request.failed(new ClientException("Cannot read from delivery due to link having been closed"));
+                    } else if (protonDelivery.available() > 0) {
+                        buffer.append(protonDelivery.readAll());
+                        request.complete(buffer.getReadableBytes());
+                    } else if (protonDelivery.isAborted()) {
+                        request.failed(new ClientDeliveryAbortedException("The remote sender has aborted this delivery"));
+                    } else if (!protonDelivery.isPartial()) {
+                        autoAcceptDeliveryIfNecessary();
+                        request.complete(-1);
+                    } else {
+                        readRequest = request;
+                    }
+                });
+
+                return receiver.session().request(receiver, request);
+            } catch (Exception e) {
+                throw new IOException("Error reading requested data", e);
+            }
+        }
+
+        private void autoAcceptDeliveryIfNecessary() {
+            if (receiver.receiverOptions().autoAccept() && !protonDelivery.isSettled()) {
+                if (!buffer.isReadable() && protonDelivery.available() == 0 &&
+                    (protonDelivery.isAborted() || !protonDelivery.isPartial())) {
+
+                    try {
+                        receiver.disposition(protonDelivery, Accepted.getInstance(), receiver.receiverOptions().autoSettle());
+                    } catch (Exception error) {
+                        LOG.trace("Caught error while attempting to auto accept the fully read delivery.", error);
+                    }
+                }
+            }
+        }
+
+        private void checkStreamStateIsValid() throws IOException {
+            if (closed.get()) {
+                throw new IOException("The InputStream has been explicity closed");
+            }
+
+            if (receiver.isClosed()) {
+                throw new IOException("Underlying receiver has closed", receiver.getFailureCause());
+            }
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamReceiver.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamReceiver.java
new file mode 100644
index 0000000..6de3f02
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamReceiver.java
@@ -0,0 +1,707 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.Source;
+import org.apache.qpid.protonj2.client.StreamDelivery;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.StreamReceiverOptions;
+import org.apache.qpid.protonj2.client.Target;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientResourceRemotelyClosedException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ClientStreamReceiver implements StreamReceiver {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientReceiver.class);
+
+    private static final AtomicIntegerFieldUpdater<ClientStreamReceiver> CLOSED_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(ClientStreamReceiver.class, "closed");
+
+    private final ClientFuture<Receiver> openFuture;
+    private final ClientFuture<Receiver> closeFuture;
+    private ClientFuture<Receiver> drainingFuture;
+    private ScheduledFuture<?> drainingTimeout;
+    private final StreamReceiverOptions options;
+    private final ClientSession session;
+    private final ScheduledExecutorService executor;
+    private final String receiverId;
+    private final Map<ClientFuture<StreamDelivery>, ScheduledFuture<?>> receiveRequests = new LinkedHashMap<>();
+
+    private org.apache.qpid.protonj2.engine.Receiver protonReceiver;
+    private volatile int closed;
+    private ClientException failureCause;
+    private volatile Source remoteSource;
+    private volatile Target remoteTarget;
+
+    public ClientStreamReceiver(ClientSession session, StreamReceiverOptions options, String receiverId, org.apache.qpid.protonj2.engine.Receiver receiver) {
+        this.options = options;
+        this.session = session;
+        this.receiverId = receiverId;
+        this.executor = session.getScheduler();
+        this.openFuture = session.getFutureFactory().createFuture();
+        this.closeFuture = session.getFutureFactory().createFuture();
+        this.protonReceiver = receiver.setLinkedResource(this);
+
+        if (options.creditWindow() > 0) {
+            protonReceiver.addCredit(options.creditWindow());
+        }
+    }
+
+    @Override
+    public ClientInstance client() {
+        return session.client();
+    }
+
+    @Override
+    public ClientConnection connection() {
+        return session.connection();
+    }
+
+    @Override
+    public ClientSession session() {
+        return session;
+    }
+
+    @Override
+    public ClientFuture<Receiver> openFuture() {
+        return openFuture;
+    }
+
+    @Override
+    public void close() {
+        try {
+            doCloseOrDetach(true, null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void close(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        try {
+            doCloseOrDetach(true, error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void detach() {
+        try {
+            doCloseOrDetach(false, null).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public void detach(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        try {
+            doCloseOrDetach(false, error).get();
+        } catch (InterruptedException | ExecutionException e) {
+            Thread.interrupted();
+        }
+    }
+
+    @Override
+    public ClientFuture<Receiver> closeAsync() {
+        return doCloseOrDetach(true, null);
+    }
+
+    @Override
+    public ClientFuture<Receiver> closeAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "Error Condition cannot be null");
+
+        return doCloseOrDetach(true, error);
+    }
+
+    @Override
+    public ClientFuture<Receiver> detachAsync() {
+        return doCloseOrDetach(false, null);
+    }
+
+    @Override
+    public ClientFuture<Receiver> detachAsync(ErrorCondition error) {
+        Objects.requireNonNull(error, "The provided Error Condition cannot be null");
+
+        return doCloseOrDetach(false, error);
+    }
+
+    private ClientFuture<Receiver> doCloseOrDetach(boolean close, ErrorCondition error) {
+        if (CLOSED_UPDATER.compareAndSet(this, 0, 1)) {
+            executor.execute(() -> {
+                if (protonReceiver.isLocallyOpen()) {
+                    try {
+                        protonReceiver.setCondition(ClientErrorCondition.asProtonErrorCondition(error));
+
+                        if (close) {
+                            protonReceiver.close();
+                        } else {
+                            protonReceiver.detach();
+                        }
+                    } catch (Throwable ignore) {
+                        closeFuture.complete(this);
+                    }
+                }
+            });
+        }
+
+        return closeFuture;
+    }
+
+    @Override
+    public StreamDelivery receive() throws ClientException {
+        return receive(-1, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public StreamDelivery receive(long timeout, TimeUnit unit) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<StreamDelivery> receive = session.getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(receive)) {
+                IncomingDelivery delivery = null;
+
+                // Scan for an unsettled delivery that isn't yet assigned to a client delivery
+                // either it is a complete delivery or the initial stage of the next incoming
+                for (IncomingDelivery unsettled : protonReceiver.unsettled()) {
+                    if (unsettled.getLinkedResource() == null) {
+                        delivery = unsettled;
+                        break;
+                    }
+                }
+
+                if (delivery == null) {
+                    if (timeout == 0) {
+                        receive.complete(null);
+                    } else {
+                        final ScheduledFuture<?> timeoutFuture;
+
+                        if (timeout > 0) {
+                            timeoutFuture = session.getScheduler().schedule(() -> {
+                                receiveRequests.remove(receive);
+                                receive.complete(null); // Timed receive returns null on failed wait.
+                            }, timeout, unit);
+                        } else {
+                            timeoutFuture = null;
+                        }
+
+                        receiveRequests.put(receive, timeoutFuture);
+                    }
+                } else {
+                    receive.complete(new ClientStreamDelivery(this, delivery));
+                }
+            }
+        });
+
+        return session.request(this, receive);
+    }
+
+    @Override
+    public StreamDelivery tryReceive() throws ClientException {
+        checkClosedOrFailed();
+        return receive(0, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public StreamReceiver addCredit(int credits) throws ClientException {
+        checkClosedOrFailed();
+        ClientFuture<StreamReceiver> creditAdded = session.getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(creditAdded)) {
+                if (options.creditWindow() != 0) {
+                    creditAdded.failed(new ClientIllegalStateException("Cannot add credit when a credit window has been configured"));
+                } else if (protonReceiver.isDraining()) {
+                    creditAdded.failed(new ClientIllegalStateException("Cannot add credit while a drain is pending"));
+                } else {
+                    try {
+                        protonReceiver.addCredit(credits);
+                        creditAdded.complete(this);
+                    } catch (Exception ex) {
+                        creditAdded.failed(ClientExceptionSupport.createNonFatalOrPassthrough(ex));
+                    }
+                }
+            }
+        });
+
+        return session.request(this, creditAdded);
+    }
+
+    @Override
+    public Future<Receiver> drain() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Receiver> drainComplete = session.getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(drainComplete)) {
+                if (protonReceiver.isDraining()) {
+                    drainComplete.failed(new ClientIllegalStateException("StreamReceiver is already draining"));
+                    return;
+                }
+
+                try {
+                    if (protonReceiver.drain()) {
+                        drainingFuture = drainComplete;
+                        drainingTimeout = session.scheduleRequestTimeout(drainingFuture, options.drainTimeout(),
+                            () -> new ClientOperationTimedOutException("Timed out waiting for remote to respond to drain request"));
+                    } else {
+                        drainComplete.complete(this);
+                    }
+                } catch (Exception ex) {
+                    drainComplete.failed(ClientExceptionSupport.createNonFatalOrPassthrough(ex));
+                }
+            }
+        });
+
+        return drainComplete;
+    }
+
+    @Override
+    public Map<String, Object> properties() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringKeyedMap(protonReceiver.getRemoteProperties());
+    }
+
+    @Override
+    public String[] offeredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonReceiver.getRemoteOfferedCapabilities());
+    }
+
+    @Override
+    public String[] desiredCapabilities() throws ClientException {
+        waitForOpenToComplete();
+        return ClientConversionSupport.toStringArray(protonReceiver.getRemoteDesiredCapabilities());
+    }
+
+    @Override
+    public String address() throws ClientException {
+        if (isDynamic()) {
+            waitForOpenToComplete();
+            return protonReceiver.getRemoteSource().getAddress();
+        } else {
+            return protonReceiver.getSource() != null ? protonReceiver.getSource().getAddress() : null;
+        }
+    }
+
+    @Override
+    public Source source() throws ClientException {
+        waitForOpenToComplete();
+        return remoteSource;
+    }
+
+    @Override
+    public Target target() throws ClientException {
+        waitForOpenToComplete();
+        return remoteTarget;
+    }
+
+    @Override
+    public long queuedDeliveries() throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<Integer> request = session.getFutureFactory().createFuture();
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(request)) {
+                int queued = 0;
+
+                // Scan for an unsettled delivery that isn't yet assigned to a client delivery
+                // either it is a complete delivery or the initial stage of the next incoming
+                for (IncomingDelivery unsettled : protonReceiver.unsettled()) {
+                    if (unsettled.getLinkedResource() == null) {
+                        queued++;
+                    }
+                }
+
+                request.complete(queued);
+            }
+        });
+
+        return session.request(this, request);
+    }
+
+    //----- Internal API for the ClientReceiver and other Client objects
+
+    ClientStreamReceiver open() {
+        protonReceiver.localOpenHandler(this::handleLocalOpen)
+                      .localCloseHandler(this::handleLocalCloseOrDetach)
+                      .localDetachHandler(this::handleLocalCloseOrDetach)
+                      .openHandler(this::handleRemoteOpen)
+                      .closeHandler(this::handleRemoteCloseOrDetach)
+                      .detachHandler(this::handleRemoteCloseOrDetach)
+                      .parentEndpointClosedHandler(this::handleParentEndpointClosed)
+                      .deliveryStateUpdatedHandler(this::handleDeliveryStateRemotelyUpdated)
+                      .deliveryReadHandler(this::handleDeliveryRead)
+                      .deliveryAbortedHandler(this::handleDeliveryAborted)
+                      .creditStateUpdateHandler(this::handleReceiverCreditUpdated)
+                      .engineShutdownHandler(this::handleEngineShutdown)
+                      .open();
+
+        return this;
+    }
+
+    void setFailureCause(ClientException failureCause) {
+        this.failureCause = failureCause;
+    }
+
+    ClientException getFailureCause() {
+        if (failureCause == null) {
+            return session.getFailureCause();
+        } else {
+            return failureCause;
+        }
+    }
+
+    String getId() {
+        return receiverId;
+    }
+
+    boolean isClosed() {
+        return closed > 0;
+    }
+
+    boolean isDynamic() {
+        return protonReceiver.getSource() != null && protonReceiver.getSource().isDynamic();
+    }
+
+    StreamReceiverOptions receiverOptions() {
+        return options;
+    }
+
+    //----- Handlers for proton receiver events
+
+    private void handleLocalOpen(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        if (options.openTimeout() > 0) {
+            executor.schedule(() -> {
+                if (!openFuture.isDone()) {
+                    immediateLinkShutdown(new ClientOperationTimedOutException("Receiver open timed out waiting for remote to respond"));
+                }
+            }, options.openTimeout(), TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private void handleLocalCloseOrDetach(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        // If not yet remotely closed we only wait for a remote close if the engine isn't
+        // already failed and we have successfully opened the sender without a timeout.
+        if (!receiver.getEngine().isShutdown() && failureCause == null && receiver.isRemotelyOpen()) {
+            final long timeout = options.closeTimeout();
+
+            if (timeout > 0) {
+                session.scheduleRequestTimeout(closeFuture, timeout, () ->
+                new ClientOperationTimedOutException("receiver close timed out waiting for remote to respond"));
+            }
+        } else {
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleRemoteOpen(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        // Check for deferred close pending and hold completion if so
+        if (receiver.getRemoteSource() != null) {
+            remoteSource = new ClientRemoteSource(receiver.getRemoteSource());
+
+            if (receiver.getRemoteTarget() != null) {
+                remoteTarget = new ClientRemoteTarget(receiver.getRemoteTarget());
+            }
+
+            replenishCreditIfNeeded();
+
+            openFuture.complete(this);
+            LOG.trace("Receiver opened successfully: {}", receiverId);
+        } else {
+            LOG.debug("Receiver opened but remote signalled close is pending: {}", receiverId);
+        }
+    }
+
+    private void handleRemoteCloseOrDetach(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        if (receiver.isLocallyOpen()) {
+            immediateLinkShutdown(ClientExceptionSupport.convertToLinkClosedException(
+                receiver.getRemoteCondition(), "Receiver remotely closed without explanation from the remote"));
+        } else {
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleParentEndpointClosed(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        // Don't react if engine was shutdown and parent closed as a result instead wait to get the
+        // shutdown notification and respond to that change.
+        if (receiver.getEngine().isRunning()) {
+            final ClientException failureCause;
+
+            if (receiver.getConnection().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(receiver.getConnection().getRemoteCondition());
+            } else if (receiver.getSession().getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToSessionClosedException(receiver.getSession().getRemoteCondition());
+            } else if (receiver.getEngine().failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(receiver.getEngine().failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientResourceRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        if (!isDynamic() && !session.getConnection().getEngine().isShutdown()) {
+            int previousCredit = protonReceiver.getCredit() + protonReceiver.unsettled().size();
+
+            if (drainingFuture != null) {
+                drainingFuture.complete(this);
+                if (drainingTimeout != null) {
+                    drainingTimeout.cancel(false);
+                    drainingTimeout = null;
+                }
+            }
+
+            protonReceiver.localCloseHandler(null);
+            protonReceiver.localDetachHandler(null);
+            protonReceiver.close();
+            protonReceiver = ClientReceiverBuilder.recreateReceiver(session, protonReceiver, options);
+            protonReceiver.setLinkedResource(this);
+            protonReceiver.addCredit(previousCredit);
+
+            open();
+        } else {
+            final Connection connection = engine.connection();
+
+            final ClientException failureCause;
+
+            if (connection.getRemoteCondition() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(connection.getRemoteCondition());
+            } else if (engine.failureCause() != null) {
+                failureCause = ClientExceptionSupport.convertToConnectionClosedException(engine.failureCause());
+            } else if (!isClosed()) {
+                failureCause = new ClientConnectionRemotelyClosedException("Remote closed without a specific error condition");
+            } else {
+                failureCause = null;
+            }
+
+            immediateLinkShutdown(failureCause);
+        }
+    }
+
+    private void handleDeliveryRead(IncomingDelivery delivery) {
+        LOG.trace("Delivery data was received: {}", delivery);
+        if (delivery.getDefaultDeliveryState() == null) {
+            delivery.setDefaultDeliveryState(Released.getInstance());
+        }
+
+        if (delivery.getLinkedResource() == null) {
+            // New delivery that can be sent to a waiting receive caller
+            if (!receiveRequests.isEmpty()) {
+                Iterator<Entry<ClientFuture<StreamDelivery>, ScheduledFuture<?>>> entries =
+                    receiveRequests.entrySet().iterator();
+
+                Entry<ClientFuture<StreamDelivery>, ScheduledFuture<?>> entry = entries.next();
+                if (entry.getValue() != null) {
+                    entry.getValue().cancel(false);
+                }
+
+                try {
+                    entry.getKey().complete(new ClientStreamDelivery(this, delivery));
+                } finally {
+                    entries.remove();
+                }
+            }
+        }
+    }
+
+    private void handleDeliveryAborted(IncomingDelivery delivery) {
+        LOG.trace("Delivery data was aborted: {}", delivery);
+        delivery.settle();
+        replenishCreditIfNeeded();
+    }
+
+    private void handleDeliveryStateRemotelyUpdated(IncomingDelivery delivery) {
+        LOG.trace("Delivery remote state was updated: {}", delivery);
+    }
+
+    private void handleReceiverCreditUpdated(org.apache.qpid.protonj2.engine.Receiver receiver) {
+        LOG.trace("Receiver credit update by remote: {}", receiver);
+
+        if (drainingFuture != null) {
+            if (receiver.getCredit() == 0) {
+                drainingFuture.complete(this);
+                if (drainingTimeout != null) {
+                    drainingTimeout.cancel(false);
+                    drainingTimeout = null;
+                }
+            }
+        }
+    }
+
+    //----- Private implementation details
+
+    void disposition(IncomingDelivery delivery, DeliveryState state, boolean settle) throws ClientException {
+        checkClosedOrFailed();
+        asyncApplyDisposition(delivery, state, settle);
+    }
+
+    private void asyncApplyDisposition(IncomingDelivery delivery, DeliveryState state, boolean settle) throws ClientException {
+        executor.execute(() -> {
+            session.getTransactionContext().disposition(delivery, state, settle);
+            replenishCreditIfNeeded();
+        });
+    }
+
+    private void replenishCreditIfNeeded() {
+        int creditWindow = options.creditWindow();
+        if (creditWindow > 0) {
+            int currentCredit = protonReceiver.getCredit();
+            if (currentCredit <= creditWindow * 0.5) {
+                int potentialPrefetch = currentCredit + protonReceiver.unsettled().size();
+
+                if (potentialPrefetch <= creditWindow * 0.7) {
+                    int additionalCredit = creditWindow - potentialPrefetch;
+
+                    LOG.trace("Consumer granting additional credit: {}", additionalCredit);
+                    try {
+                        protonReceiver.addCredit(additionalCredit);
+                    } catch (Exception ex) {
+                        LOG.debug("Error caught during credit top-up", ex);
+                    }
+                }
+            }
+        }
+    }
+
+    private void waitForOpenToComplete() throws ClientException {
+        if (!openFuture.isComplete() || openFuture.isFailed()) {
+            try {
+                openFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                Thread.interrupted();
+                if (failureCause != null) {
+                    throw failureCause;
+                } else {
+                    throw ClientExceptionSupport.createNonFatalOrPassthrough(e.getCause());
+                }
+            }
+        }
+    }
+
+    private boolean notClosedOrFailed(ClientFuture<?> request) {
+        if (isClosed()) {
+            request.failed(new ClientIllegalStateException("The Receiver was explicity closed", failureCause));
+            return false;
+        } else if (failureCause != null) {
+            request.failed(failureCause);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    protected void checkClosedOrFailed() throws ClientException {
+        if (isClosed()) {
+            throw new ClientIllegalStateException("The Receiver was explicity closed", failureCause);
+        } else if (failureCause != null) {
+            throw failureCause;
+        }
+    }
+
+    private void immediateLinkShutdown(ClientException failureCause) {
+        if (this.failureCause == null) {
+            this.failureCause = failureCause;
+        }
+
+        try {
+            if (protonReceiver.isRemotelyDetached()) {
+                protonReceiver.detach();
+            } else {
+                protonReceiver.close();
+            }
+        } catch (Exception ignore) {
+            // Ignore
+        } finally {
+            session.closeAsync();
+        }
+
+        receiveRequests.forEach((future, timeout) -> {
+            if (timeout != null) {
+                timeout.cancel(false);
+            }
+
+            if (failureCause != null) {
+                future.failed(failureCause);
+            } else {
+                future.failed(new ClientResourceRemotelyClosedException("The Stream Receiver has closed"));
+            }
+        });
+
+        protonReceiver.unsettled().forEach((delivery) -> {
+            if (delivery.getLinkedResource() != null) {
+                try {
+                    delivery.getLinkedResource(ClientStreamDelivery.class).handleReceiverClosed(this);
+                } catch (Exception ex) {}
+            }
+        });
+
+        if (failureCause != null) {
+            openFuture.failed(failureCause);
+            if (drainingFuture != null) {
+                drainingFuture.failed(failureCause);
+            }
+        } else {
+            openFuture.complete(this);
+            if (drainingFuture != null) {
+                drainingFuture.failed(new ClientResourceRemotelyClosedException("The Receiver has been closed"));
+            }
+        }
+
+        if (drainingTimeout != null) {
+            drainingTimeout.cancel(false);
+            drainingTimeout = null;
+        }
+
+        closeFuture.complete(this);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamReceiverMessage.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamReceiverMessage.java
new file mode 100644
index 0000000..36f3c5a
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamReceiverMessage.java
@@ -0,0 +1,928 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.StreamReceiverMessage;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientMessageFormatViolationException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.codec.DecodeEOFException;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.StreamDecoder;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BinaryTypeDecoder;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Streamed message delivery context used to request reads of possible split framed
+ * {@link Transfer} payload's that comprise a single large overall message.
+ */
+public final class ClientStreamReceiverMessage implements StreamReceiverMessage {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientStreamReceiverMessage.class);
+
+    private enum StreamState {
+        IDLE,
+        HEADER_READ,
+        DELIVERY_ANNOTATIONS_READ,
+        MESSAGE_ANNOTATIONS_READ,
+        PROPERTIES_READ,
+        APPLICATION_PROPERTIES_READ,
+        BODY_PENDING,
+        BODY_READABLE,
+        FOOTER_READ,
+        DECODE_ERROR
+    }
+
+    private final ClientStreamReceiver receiver;
+    private final ClientStreamDelivery delivery;
+    private final InputStream deliveryStream;
+    private final IncomingDelivery protonDelivery;
+    private final StreamDecoder protonDecoder = ProtonStreamDecoderFactory.create();
+    private final StreamDecoderState decoderState = protonDecoder.newDecoderState();
+
+    private Header header;
+    private DeliveryAnnotations deliveryAnnotations;
+    private MessageAnnotations annotations;
+    private Properties properties;
+    private ApplicationProperties applicationProperties;
+    private Footer footer;
+
+    private StreamState currentState = StreamState.IDLE;
+    private MessageBodyInputStream bodyStream;
+
+    ClientStreamReceiverMessage(ClientStreamReceiver receiver, ClientStreamDelivery delivery, InputStream deliveryStream) {
+        this.receiver = receiver;
+        this.delivery = delivery;
+        this.deliveryStream = deliveryStream;
+        this.protonDelivery = delivery.getProtonDelivery();
+    }
+
+    @Override
+    public ClientStreamReceiver receiver() {
+        return receiver;
+    }
+
+    @Override
+    public ClientStreamDelivery delivery() {
+        return delivery;
+    }
+
+    @Override
+    public boolean aborted() {
+        if (protonDelivery != null) {
+            return protonDelivery.isAborted();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean completed() {
+        if (protonDelivery != null) {
+            return !protonDelivery.isPartial() && !protonDelivery.isAborted();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int messageFormat() throws ClientException {
+        return protonDelivery != null ? protonDelivery.getMessageFormat() : 0;
+    }
+
+    @Override
+    public StreamReceiverMessage messageFormat(int messageFormat) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiverMessage");
+    }
+
+    //----- Header API implementation
+
+    @Override
+    public boolean durable() throws ClientException {
+        return header() != null ? header.isDurable() : false;
+    }
+
+    @Override
+    public StreamReceiverMessage durable(boolean durable) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public byte priority() throws ClientException {
+        return header() != null ? header.getPriority() : Header.DEFAULT_PRIORITY;
+    }
+
+    @Override
+    public StreamReceiverMessage priority(byte priority) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public long timeToLive() throws ClientException {
+        return header() != null ? header.getTimeToLive() : Header.DEFAULT_TIME_TO_LIVE;
+    }
+
+    @Override
+    public StreamReceiverMessage timeToLive(long timeToLive) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public boolean firstAcquirer() throws ClientException {
+        return header() != null ? header.isFirstAcquirer() : Header.DEFAULT_FIRST_ACQUIRER;
+    }
+
+    @Override
+    public StreamReceiverMessage firstAcquirer(boolean firstAcquirer) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public long deliveryCount() throws ClientException {
+        return header() != null ? header.getDeliveryCount() : Header.DEFAULT_DELIVERY_COUNT;
+    }
+
+    @Override
+    public StreamReceiverMessage deliveryCount(long deliveryCount) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public Header header() throws ClientException {
+        ensureStreamDecodedTo(StreamState.HEADER_READ);
+        return header;
+    }
+
+    @Override
+    public StreamReceiverMessage header(Header header) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    //----- Properties API implementation
+
+    @Override
+    public Object messageId() throws ClientException {
+        if (properties() != null) {
+            return properties().getMessageId();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage messageId(Object messageId) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public byte[] userId() throws ClientException {
+        if (properties() != null) {
+            byte[] copyOfUserId = null;
+            if (properties != null && properties().getUserId() != null) {
+                copyOfUserId = properties().getUserId().arrayCopy();
+            }
+
+            return copyOfUserId;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage userId(byte[] userId) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String to() throws ClientException {
+        if (properties() != null) {
+            return properties().getTo();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage to(String to) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String subject() throws ClientException {
+        if (properties() != null) {
+            return properties().getSubject();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage subject(String subject) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String replyTo() throws ClientException {
+        if (properties() != null) {
+            return properties().getReplyTo();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage replyTo(String replyTo) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public Object correlationId() throws ClientException {
+        if (properties() != null) {
+            return properties().getCorrelationId();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage correlationId(Object correlationId) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String contentType() throws ClientException {
+        if (properties() != null) {
+            return properties().getContentType();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage contentType(String contentType) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String contentEncoding() throws ClientException {
+        if (properties() != null) {
+            return properties().getContentEncoding();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Message<?> contentEncoding(String contentEncoding) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public long absoluteExpiryTime() throws ClientException {
+        if (properties() != null) {
+            return properties().getAbsoluteExpiryTime();
+        } else {
+            return 0l;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage absoluteExpiryTime(long expiryTime) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public long creationTime() throws ClientException {
+        if (properties() != null) {
+            return properties().getCreationTime();
+        } else {
+            return 0l;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage creationTime(long createTime) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String groupId() throws ClientException {
+        if (properties() != null) {
+            return properties().getGroupId();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage groupId(String groupId) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public int groupSequence() throws ClientException {
+        if (properties() != null) {
+            return (int) properties().getGroupSequence();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage groupSequence(int groupSequence) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public String replyToGroupId() throws ClientException {
+        if (properties() != null) {
+            return properties().getReplyToGroupId();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage replyToGroupId(String replyToGroupId) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public Properties properties() throws ClientException {
+        ensureStreamDecodedTo(StreamState.PROPERTIES_READ);
+        return properties;
+    }
+
+    @Override
+    public StreamReceiverMessage properties(Properties properties) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    //----- Delivery Annotations API (Internal Access Only)
+
+    DeliveryAnnotations deliveryAnnotations() throws ClientException {
+        ensureStreamDecodedTo(StreamState.DELIVERY_ANNOTATIONS_READ);
+        return deliveryAnnotations;
+    }
+
+    //----- Message Annotations API
+
+    @Override
+    public Object annotation(String key) throws ClientException {
+        if (hasAnnotations()) {
+            return annotations.getValue().get(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean hasAnnotation(String key) throws ClientException {
+        if (hasAnnotations()) {
+            return annotations.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasAnnotations() throws ClientException {
+        ensureStreamDecodedTo(StreamState.MESSAGE_ANNOTATIONS_READ);
+        return annotations != null && annotations.getValue() != null && annotations.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeAnnotation(String key) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public StreamReceiverMessage forEachAnnotation(BiConsumer<String, Object> action) throws ClientException {
+        if (hasAnnotations()) {
+            annotations.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public StreamReceiverMessage annotation(String key, Object value) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public MessageAnnotations annotations() throws ClientException {
+        if (hasAnnotations()) {
+            return annotations;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage annotations(MessageAnnotations messageAnnotations) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    //----- Application Properties API
+
+    @Override
+    public Object property(String key) throws ClientException {
+        if (hasProperties()) {
+            return applicationProperties.getValue().get(key);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean hasProperty(String key) throws ClientException {
+        if (hasProperties()) {
+            return applicationProperties.getValue().containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasProperties() throws ClientException {
+        ensureStreamDecodedTo(StreamState.APPLICATION_PROPERTIES_READ);
+        return applicationProperties != null &&
+               applicationProperties.getValue() != null &&
+               applicationProperties.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeProperty(String key) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public StreamReceiverMessage forEachProperty(BiConsumer<String, Object> action) throws ClientException {
+        if (hasProperties()) {
+            applicationProperties.getValue().forEach(action);
+        }
+        return this;
+    }
+
+    @Override
+    public StreamReceiverMessage property(String key, Object value) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public ApplicationProperties applicationProperties() throws ClientException {
+        if (hasProperties()) {
+            return applicationProperties;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage applicationProperties(ApplicationProperties applicationProperties) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    //----- Message Footer API
+
+    @Override
+    public Object footer(String key) throws ClientException {
+        if (hasFooters()) {
+            return footer.getValue().get(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean hasFooter(String key) throws ClientException {
+        if (hasFooters()) {
+            return footer.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasFooters() throws ClientException {
+        ensureStreamDecodedTo(StreamState.BODY_READABLE);
+
+        if (currentState != StreamState.FOOTER_READ) {
+            if (currentState == StreamState.DECODE_ERROR) {
+                throw new ClientException("Cannot read Footer due to decoding error in message payload");
+            } else {
+                throw new ClientIllegalStateException("Cannot read message Footer until message body fully read");
+            }
+        }
+
+        return footer != null && footer.getValue() != null && footer.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeFooter(String key) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public StreamReceiverMessage forEachFooter(BiConsumer<String, Object> action) throws ClientException {
+        if (hasFooters()) {
+            footer.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public StreamReceiverMessage footer(String key, Object value) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    @Override
+    public Footer footer() throws ClientException {
+        if (hasFooters()) {
+            return footer;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public StreamReceiverMessage footer(Footer footer) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot write to a StreamReceiveMessage");
+    }
+
+    //----- Message Body Access API
+
+    @Override
+    public StreamReceiverMessage addBodySection(Section<?> bodySection) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot encode from an StreamReceiverMessage instance.");
+    }
+
+    @Override
+    public StreamReceiverMessage bodySections(Collection<Section<?>> sections) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot encode from an StreamReceiverMessage instance.");
+    }
+
+    @Override
+    public Collection<Section<?>> bodySections() throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot decode all body sections from a StreamReceiverMessage instance.");
+    }
+
+    @Override
+    public StreamReceiverMessage forEachBodySection(Consumer<Section<?>> consumer) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot decode all body sections from a StreamReceiverMessage instance.");
+    }
+
+    @Override
+    public StreamReceiverMessage clearBodySections() throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot encode from an StreamReceiverMessage instance.");
+    }
+
+    @Override
+    public InputStream body() throws ClientException {
+        if (currentState.ordinal() > StreamState.BODY_READABLE.ordinal()) {
+            if (currentState == StreamState.DECODE_ERROR) {
+                throw new ClientException("Cannot read body due to decoding error in message payload");
+            } else if (bodyStream != null) {
+                throw new ClientIllegalStateException("Cannot read body from message whose body has already been read.");
+            }
+        }
+
+        ensureStreamDecodedTo(StreamState.BODY_READABLE);
+
+        return bodyStream;
+    }
+
+    @Override
+    public StreamReceiverMessage body(InputStream value) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot encode from an StreamReceiverMessage instance.");
+    }
+
+    //----- AdvancedMessage encoding API implementation.
+
+    @Override
+    public ProtonBuffer encode(Map<String, Object> deliveryAnnotations) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot encode from an StreamReceiverMessage instance.");
+    }
+
+    //----- Internal Streamed Delivery API and support methods
+
+    private void checkClosedOrAborted() throws ClientIllegalStateException {
+        if (receiver.isClosed()) {
+            throw new ClientIllegalStateException("The parent Receiver instance has already been closed.");
+        }
+
+        if (aborted()) {
+            throw new ClientIllegalStateException("The incoming delivery was aborted.");
+        }
+    }
+
+    private void ensureStreamDecodedTo(StreamState desiredState) throws ClientException {
+        checkClosedOrAborted();
+
+        while (currentState.ordinal() < desiredState.ordinal()) {
+            try {
+                final StreamTypeDecoder<?> decoder;
+                try {
+                    decoder = protonDecoder.readNextTypeDecoder(deliveryStream, decoderState);
+                } catch (DecodeEOFException eof) {
+                    currentState = StreamState.FOOTER_READ;
+                    break;
+                }
+
+                final Class<?> typeClass = decoder.getTypeClass();
+
+                if (typeClass == Header.class) {
+                    header = (Header) decoder.readValue(deliveryStream, decoderState);
+                    currentState = StreamState.HEADER_READ;
+                } else if (typeClass == DeliveryAnnotations.class) {
+                    deliveryAnnotations = (DeliveryAnnotations) decoder.readValue(deliveryStream, decoderState);
+                    currentState = StreamState.DELIVERY_ANNOTATIONS_READ;
+                } else if (typeClass == MessageAnnotations.class) {
+                    annotations = (MessageAnnotations) decoder.readValue(deliveryStream, decoderState);
+                    currentState = StreamState.MESSAGE_ANNOTATIONS_READ;
+                } else if (typeClass == Properties.class) {
+                    properties = (Properties) decoder.readValue(deliveryStream, decoderState);
+                    currentState = StreamState.PROPERTIES_READ;
+                } else if (typeClass == ApplicationProperties.class) {
+                    applicationProperties = (ApplicationProperties) decoder.readValue(deliveryStream, decoderState);
+                    currentState = StreamState.APPLICATION_PROPERTIES_READ;
+                } else if (typeClass == AmqpSequence.class) {
+                    currentState = StreamState.BODY_READABLE;
+                    if (bodyStream == null) {
+                        bodyStream = new AmqpSequenceInputStream(deliveryStream);
+                    }
+                } else if (typeClass == AmqpValue.class) {
+                    currentState = StreamState.BODY_READABLE;
+                    if (bodyStream == null) {
+                        bodyStream = new AmqpValueInputStream(deliveryStream);
+                    }
+                } else if (typeClass == Data.class) {
+                    currentState = StreamState.BODY_READABLE;
+                    if (bodyStream == null) {
+                        bodyStream = new DataSectionInputStream(deliveryStream);
+                    }
+                } else if (typeClass == Footer.class) {
+                    footer = (Footer) decoder.readValue(deliveryStream, decoderState);
+                    currentState = StreamState.FOOTER_READ;
+                } else {
+                    throw new ClientMessageFormatViolationException("Incoming message carries unknown Section");
+                }
+            } catch (ClientMessageFormatViolationException | DecodeException ex) {
+                currentState = StreamState.DECODE_ERROR;
+                if (deliveryStream != null) {
+                    try {
+                        deliveryStream.close();
+                    } catch (IOException e) {
+                    }
+                }
+
+                // TODO: At the moment there is no automatic rejection or release etc
+                //       of the delivery.  The user is expected to apply a disposition in
+                //       response to this error that initiates the desired outcome.  We
+                //       could look to add auto settlement with a configured outcome in
+                //       the future.
+
+                throw ClientExceptionSupport.createNonFatalOrPassthrough(ex);
+            }
+        }
+    }
+
+    //----- Internal InputStream implementations
+
+    private abstract class MessageBodyInputStream extends FilterInputStream {
+
+        protected boolean closed;
+        protected long remainingSectionBytes = 0;
+
+        protected MessageBodyInputStream(InputStream deliveryStream) throws ClientException {
+            super(deliveryStream);
+
+            validateAndScanNextSection();
+        }
+
+        @Override
+        public void close() throws IOException {
+            try {
+                // This will check is another body section is present or if there was a footer and if
+                // a Footer is present it will be decoded and the message payload should be fully consumed
+                // at that point.  Otherwise the underlying raw InputStream will handle the task of
+                // discarding pending bytes for the message to ensure the receiver does not still on
+                // waiting for session window to be opened.
+                if (remainingSectionBytes == 0) {
+                    ensureStreamDecodedTo(StreamState.FOOTER_READ);
+                }
+            } catch (ClientException e) {
+                throw new IOException("Caught error while attempting to advabce past remaining message body");
+            } finally {
+                this.closed = true;
+                super.close();
+            }
+        }
+
+        @Override
+        public int read() throws IOException {
+            checkClosed();
+
+            while (true) {
+                if (remainingSectionBytes == 0 && !tryMoveToNextBodySection()) {
+                    return -1;  // Cannot read any further.
+                } else {
+                    remainingSectionBytes--;
+                    return super.read();
+                }
+            }
+        }
+
+        @Override
+        public int read(byte target[], int offset, int length) throws IOException {
+            checkClosed();
+
+            int bytesRead = 0;
+
+            while (bytesRead != length) {
+                if (remainingSectionBytes == 0 && !tryMoveToNextBodySection()) {
+                    bytesRead = bytesRead > 0 ? bytesRead : -1;
+                    break; // We are at the end of the body sections
+                }
+
+                final int readChunk = (int) Math.min(remainingSectionBytes, length - bytesRead);
+                final int actualRead = super.read(target, offset + bytesRead, readChunk);
+
+                if (actualRead > 0) {
+                    bytesRead += actualRead;
+                    remainingSectionBytes -= actualRead;
+                }
+            }
+
+            return bytesRead;
+        }
+
+        @Override
+        public long skip(long skipSize) throws IOException {
+            checkClosed();
+
+            int bytesSkipped = 0;
+
+            while (bytesSkipped != skipSize) {
+                if (remainingSectionBytes == 0 && !tryMoveToNextBodySection()) {
+                    bytesSkipped = bytesSkipped > 0 ? bytesSkipped : -1;
+                    break; // We are at the end of the body sections
+                }
+
+                final long skipChunk = (int) Math.min(remainingSectionBytes, skipSize - bytesSkipped);
+                final long actualSkip = super.skip(skipChunk);
+
+                // Ensure we handle wrapped stream not honoring the API and returning -1 for EOF
+                if (actualSkip > 0) {
+                    bytesSkipped += actualSkip;
+                    remainingSectionBytes -= actualSkip;
+                }
+            }
+
+            return bytesSkipped;
+        }
+
+        public abstract Class<?> getBodyTypeClass();
+
+        protected abstract void validateAndScanNextSection() throws ClientException;
+
+        protected boolean tryMoveToNextBodySection() throws IOException {
+            try {
+                if (currentState != StreamState.FOOTER_READ) {
+                    currentState = StreamState.BODY_PENDING;
+                    ensureStreamDecodedTo(StreamState.BODY_READABLE);
+                    if (currentState == StreamState.BODY_READABLE) {
+                        validateAndScanNextSection();
+                        return true;
+                    }
+                }
+
+                return false;
+            } catch (ClientException e) {
+                throw new IOException(e);
+            }
+        }
+
+        protected void checkClosed() throws IOException {
+            if (closed) {
+                throw new IOException("Stream was closed previously");
+            }
+        }
+    }
+
+    private class DataSectionInputStream extends MessageBodyInputStream {
+
+        public DataSectionInputStream(InputStream deliveryStream) throws ClientException {
+            super(deliveryStream);
+        }
+
+        @Override
+        public Class<?> getBodyTypeClass() {
+            return byte[].class;
+        }
+
+        @Override
+        protected void validateAndScanNextSection() throws ClientException {
+            final StreamTypeDecoder<?> typeDecoder =
+                protonDecoder.readNextTypeDecoder(deliveryStream, decoderState);
+
+            if (typeDecoder.getTypeClass() == Binary.class) {
+                LOG.trace("Data Section of size {} ready for read.", remainingSectionBytes);
+                BinaryTypeDecoder binaryDecoder = (BinaryTypeDecoder) typeDecoder;
+                remainingSectionBytes = binaryDecoder.readSize(deliveryStream);
+            } else if (typeDecoder.getTypeClass() == Void.class) {
+                // Null body in the Data section which can be skipped.
+                LOG.trace("Data Section with no Binary payload read and skipped.");
+                remainingSectionBytes = 0;
+            } else {
+                throw new DecodeException("Unknown payload in body of Data Section encoding.");
+            }
+        }
+    }
+
+    private class AmqpSequenceInputStream extends MessageBodyInputStream {
+
+        public AmqpSequenceInputStream(InputStream deliveryStream) throws ClientException {
+            super(deliveryStream);
+        }
+
+        @Override
+        public Class<?> getBodyTypeClass() {
+            return List.class;
+        }
+
+        @Override
+        protected void validateAndScanNextSection() throws ClientException {
+            throw new DecodeException("Cannot read the payload of an AMQP Sequence payload.");
+        }
+    }
+
+    private class AmqpValueInputStream extends MessageBodyInputStream {
+
+        private Class<?> bodyTypeClass = Void.class;
+
+        public AmqpValueInputStream(InputStream deliveryStream) throws ClientException {
+            super(deliveryStream);
+        }
+
+        @Override
+        public Class<?> getBodyTypeClass() {
+            return bodyTypeClass;
+        }
+
+        @Override
+        protected void validateAndScanNextSection() throws ClientException {
+            throw new DecodeException("Cannot read the payload of an AMQP Sequence payload.");
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSender.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSender.java
new file mode 100644
index 0000000..92a9e38
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSender.java
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderOptions;
+import org.apache.qpid.protonj2.client.StreamTracker;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+
+public final class ClientStreamSender extends ClientSender implements StreamSender {
+
+    private final StreamSenderOptions options;
+
+    public ClientStreamSender(ClientSession session, StreamSenderOptions options, String senderId, org.apache.qpid.protonj2.engine.Sender protonSender) {
+        super(session, options, senderId, protonSender);
+
+        this.options = new StreamSenderOptions(options);
+    }
+
+    @Override
+    public StreamTracker send(Message<?> message) throws ClientException {
+        checkClosedOrFailed();
+        return (StreamTracker) sendMessage(ClientMessageSupport.convertMessage(message), null, true);
+    }
+
+    @Override
+    public StreamTracker send(Message<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        checkClosedOrFailed();
+        return (StreamTracker) sendMessage(ClientMessageSupport.convertMessage(message), null, true);
+    }
+
+    @Override
+    public StreamTracker trySend(Message<?> message) throws ClientException {
+        checkClosedOrFailed();
+        return (StreamTracker) sendMessage(ClientMessageSupport.convertMessage(message), null, false);
+    }
+
+    @Override
+    public StreamTracker trySend(Message<?> message, Map<String, Object> deliveryAnnotations) throws ClientException {
+        checkClosedOrFailed();
+        return (StreamTracker) sendMessage(ClientMessageSupport.convertMessage(message), null, false);
+    }
+
+    @Override
+    public ClientStreamSenderMessage beginMessage() throws ClientException {
+        return beginMessage(null);
+    }
+
+    @Override
+    public ClientStreamSenderMessage beginMessage(Map<String, Object> deliveryAnnotations) throws ClientException {
+        checkClosedOrFailed();
+        final ClientFuture<ClientStreamSenderMessage> request = session.getFutureFactory().createFuture();
+        final DeliveryAnnotations annotations;
+
+        if (deliveryAnnotations != null) {
+            annotations = new DeliveryAnnotations(StringUtils.toSymbolKeyedMap(deliveryAnnotations));
+        } else {
+            annotations = null;
+        }
+
+        executor.execute(() -> {
+            if (protonSender.current() != null) {
+                request.failed(new ClientIllegalStateException(
+                    "Cannot initiate a new streaming send until the previous one is complete"));
+            } else {
+                // Grab the next delivery and hold for stream writes, no other sends
+                // can occur while we hold the delivery.
+                final OutgoingDelivery streamDelivery = protonSender.next();
+                final ClientStreamTracker streamTracker = createTracker(streamDelivery);
+
+                streamDelivery.setLinkedResource(streamTracker);
+
+                request.complete(new ClientStreamSenderMessage(this, streamTracker, annotations));
+            }
+        });
+
+        return session.request(this, request);
+    }
+
+    //----- Internal API
+
+    @Override
+    StreamSenderOptions options() {
+        return this.options;
+    }
+
+    @Override
+    ClientStreamSender open() {
+        return (ClientStreamSender) super.open();
+    }
+
+    StreamTracker sendMessage(ClientStreamSenderMessage context, AdvancedMessage<?> message) throws ClientException {
+        final ClientFuture<Tracker> operation = session.getFutureFactory().createFuture();
+        final ProtonBuffer buffer = message.encode(null);
+        final ClientOutgoingEnvelope envelope = new ClientOutgoingEnvelope(
+            this, context.getProtonDelivery(), message.messageFormat(), buffer, context.completed(), operation);
+
+        executor.execute(() -> {
+            if (notClosedOrFailed(operation)) {
+                try {
+                    if (protonSender.isSendable()) {
+                        session.getTransactionContext().send(envelope, null, isSendingSettled());
+                    } else {
+                        addToHeadOfBlockedQueue(envelope);
+                    }
+                } catch (Exception error) {
+                    operation.failed(ClientExceptionSupport.createNonFatalOrPassthrough(error));
+                }
+            }
+        });
+
+        return (StreamTracker) session.request(this, operation);
+    }
+
+    @Override
+    protected ClientStreamTracker createTracker(OutgoingDelivery delivery) {
+        return new ClientStreamTracker(this, delivery);
+    }
+
+    @Override
+    protected ClientNoOpStreamTracker createNoOpTracker() {
+        return new ClientNoOpStreamTracker(this);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSenderMessage.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSenderMessage.java
new file mode 100644
index 0000000..dc84c27
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSenderMessage.java
@@ -0,0 +1,1057 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonCompositeBuffer;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.OutputStreamOptions;
+import org.apache.qpid.protonj2.client.StreamSenderMessage;
+import org.apache.qpid.protonj2.client.StreamSenderOptions;
+import org.apache.qpid.protonj2.client.StreamTracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+
+/**
+ * Streaming Sender context used to multiple send operations that comprise the payload
+ * of a single larger message transfer.
+ */
+final class ClientStreamSenderMessage implements StreamSenderMessage {
+
+    private static final int DATA_SECTION_HEADER_ENCODING_SIZE = 8;
+
+    // Standard encoding data for a Data Section (Requires four byte size written before writing the actual data)
+    private static final byte[] DATA_SECTION_PREAMBLE = { EncodingCodes.DESCRIBED_TYPE_INDICATOR,
+                                                          EncodingCodes.SMALLULONG,
+                                                          Data.DESCRIPTOR_CODE.byteValue(),
+                                                          EncodingCodes.VBIN32 };
+
+    private enum StreamState {
+        PREAMBLE,
+        BODY_WRITABLE,
+        BODY_WRITTING,
+        COMPLETE,
+        ABORTED
+    }
+
+    private final ClientStreamSender sender;
+    private final DeliveryAnnotations deliveryAnnotations;
+    private final int writeBufferSize;
+    private final StreamMessagePacket streamMessagePacket = new StreamMessagePacket();
+    private final ClientStreamTracker tracker;
+
+    private Header header;
+    private MessageAnnotations annotations;
+    private Properties properties;
+    private ApplicationProperties applicationProperties;
+    private Footer footer;
+
+    private ProtonBuffer buffer;
+    private volatile int messageFormat;
+    private StreamState currentState = StreamState.PREAMBLE;
+
+    ClientStreamSenderMessage(ClientStreamSender sender, ClientStreamTracker tracker, DeliveryAnnotations deliveryAnnotations) {
+        this.sender = sender;
+        this.deliveryAnnotations = deliveryAnnotations;
+        this.tracker = tracker;
+
+        if (sender.options().writeBufferSize() > 0) {
+            writeBufferSize = Math.max(StreamSenderOptions.MIN_BUFFER_SIZE_LIMIT, sender.options().writeBufferSize());
+        } else {
+            writeBufferSize = Math.max(StreamSenderOptions.MIN_BUFFER_SIZE_LIMIT,
+                                  (int) sender.getProtonSender().getConnection().getMaxFrameSize());
+        }
+    }
+
+    OutgoingDelivery getProtonDelivery() {
+        return tracker.delivery();
+    }
+
+    @Override
+    public ClientStreamSender sender() {
+        return sender;
+    }
+
+    @Override
+    public StreamTracker tracker() {
+        return completed() ? tracker : null;
+    }
+
+    @Override
+    public int messageFormat() throws ClientException {
+        return messageFormat;
+    }
+
+    @Override
+    public ClientStreamSenderMessage messageFormat(int messageFormat) throws ClientException {
+        if (currentState != StreamState.PREAMBLE) {
+            throw new ClientIllegalStateException("Cannot set message format after body writes have started.");
+        }
+
+        this.messageFormat = messageFormat;
+
+        return this;
+    }
+
+    private void doFlush() throws ClientException {
+        if (buffer != null && buffer.isReadable()) {
+            try {
+                sender.sendMessage(this, streamMessagePacket);
+            } finally {
+                buffer = null;
+            }
+        }
+    }
+
+    @Override
+    public ClientStreamSenderMessage abort() throws ClientException {
+        if (completed()) {
+            throw new ClientIllegalStateException("Cannot abort an already completed send context");
+        }
+
+        if (!aborted()) {
+            currentState = StreamState.ABORTED;
+            sender.abort(getProtonDelivery(), tracker);
+        }
+
+        return this;
+    }
+
+    @Override
+    public boolean aborted() {
+        return currentState == StreamState.ABORTED;
+    }
+
+    @Override
+    public ClientStreamSenderMessage complete() throws ClientException {
+        if (aborted()) {
+            throw new ClientIllegalStateException("Cannot complete an already aborted send context");
+        }
+
+        if (!completed()) {
+            // This may result in completion if the write surpasses the buffer limit but we still
+            // need to check in case it does not, or if there are no footers...
+            if (footer != null) {
+                write(footer);
+            }
+
+            currentState = StreamState.COMPLETE;
+
+            // If there is buffered data we can flush and complete in one Transfer
+            // frame otherwise we only need to do work if there was ever a send on
+            // this context which would imply we have a Tracker and a Delivery.
+            if (buffer != null && buffer.isReadable()) {
+                doFlush();
+            } else {
+                sender.complete(getProtonDelivery(), tracker);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public boolean completed() {
+        return currentState == StreamState.COMPLETE;
+    }
+
+    @Override
+    public Message<OutputStream> body(OutputStream value) throws ClientUnsupportedOperationException {
+        throw new ClientUnsupportedOperationException("Cannot set an OutputStream body on a StreamSenderMessage");
+    }
+
+    @Override
+    public StreamSenderMessage addBodySection(Section<?> bodySection) throws ClientException {
+        if (completed()) {
+            throw new ClientIllegalStateException("Cannot add more body sections to a completed message");
+        }
+
+        if (aborted()) {
+            throw new ClientIllegalStateException("Cannot add more body sections to an aborted message");
+        }
+
+        if (currentState == StreamState.BODY_WRITTING) {
+            throw new ClientIllegalStateException("Cannot add more body sections while an OutputStream is active");
+        }
+
+        transitionToWritableState();
+
+        appenedDataToBuffer(ClientMessageSupport.encodeSection(bodySection, ProtonByteBufferAllocator.DEFAULT.allocate()));
+
+        return this;
+    }
+
+    @Override
+    public StreamSenderMessage bodySections(Collection<Section<?>> sections) throws ClientException {
+        Objects.requireNonNull(sections, "Cannot set body sections with a null Collection");
+
+        for (Section<?> section : sections) {
+            addBodySection(section);
+        }
+
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<Section<?>> bodySections() throws ClientException {
+        // Body sections are not held in memory but encoded as offered so they cannot be returned.
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public StreamSenderMessage forEachBodySection(Consumer<Section<?>> consumer) throws ClientException {
+        // Body sections are not held in memory but encoded as offered so they are not iterable.
+        return this;
+    }
+
+    @Override
+    public StreamSenderMessage clearBodySections() throws ClientException {
+        // Body sections are not held in memory but encoded as offered so they cannot be cleared.
+        return this;
+    }
+
+    @Override
+    public OutputStream body() throws ClientException {
+        return body(new OutputStreamOptions());
+    }
+
+    @Override
+    public OutputStream body(OutputStreamOptions options) throws ClientException {
+        if (completed()) {
+            throw new ClientIllegalStateException("Cannot create an OutputStream from a completed send context");
+        }
+
+        if (aborted()) {
+            throw new ClientIllegalStateException("Cannot create an OutputStream from a aborted send context");
+        }
+
+        if (currentState == StreamState.BODY_WRITTING) {
+            throw new ClientIllegalStateException("Cannot add more body sections while an OutputStream is active");
+        }
+
+        transitionToWritableState();
+
+        ProtonBuffer streamBuffer = ProtonByteBufferAllocator.DEFAULT.allocate(writeBufferSize, writeBufferSize);
+
+        if (options.bodyLength() > 0) {
+            return new SingularDataSectionOutputStream(options, streamBuffer);
+        } else {
+            return new MultipleDataSectionsOutputStream(options, streamBuffer);
+        }
+    }
+
+    @Override
+    public OutputStream rawOutputStream() throws ClientException {
+        if (completed()) {
+            throw new ClientIllegalStateException("Cannot create an OutputStream from a completed send context");
+        }
+
+        if (aborted()) {
+            throw new ClientIllegalStateException("Cannot create an OutputStream from a aborted send context");
+        }
+
+        if (currentState == StreamState.BODY_WRITTING) {
+            throw new ClientIllegalStateException("Cannot add more body sections while an OutputStream is active");
+        }
+
+        transitionToWritableState();
+
+        return new SendContextRawBytesOutputStream(ProtonByteBufferAllocator.DEFAULT.allocate(writeBufferSize, writeBufferSize));
+    }
+
+    //----- OutputStream implementation for the Send Context
+
+    private abstract class StreamMessageOutputStream extends OutputStream {
+
+        protected final AtomicBoolean closed = new AtomicBoolean();
+        protected final OutputStreamOptions options;
+        protected final ProtonBuffer streamBuffer;
+
+        protected int bytesWritten;
+
+        public StreamMessageOutputStream(OutputStreamOptions options, ProtonBuffer buffer) {
+            this.options = options;
+            this.streamBuffer = buffer;
+
+            // Stream takes control of state until closed.
+            currentState = StreamState.BODY_WRITTING;
+        }
+
+        @Override
+        public void write(int value) throws IOException {
+            checkClosed();
+            checkOutputLimitReached(1);
+            streamBuffer.writeByte(value);
+            if (!streamBuffer.isWritable()) {
+                flush();
+            }
+            bytesWritten++;
+        }
+
+        @Override
+        public void write(byte bytes[]) throws IOException {
+            write(bytes, 0, bytes.length);
+        }
+
+        @Override
+        public void write(byte bytes[], int offset, int length) throws IOException {
+            checkClosed();
+            checkOutputLimitReached(length);
+            if (streamBuffer.getWritableBytes() >= length) {
+                streamBuffer.writeBytes(bytes, offset, length);
+                bytesWritten += length;
+                if (!streamBuffer.isWritable()) {
+                    flush();
+                }
+            } else {
+                int remaining = length;
+
+                while (remaining > 0) {
+                    int toWrite = Math.min(remaining, streamBuffer.getWritableBytes());
+                    bytesWritten += toWrite;
+                    streamBuffer.writeBytes(bytes, offset + (length - remaining), toWrite);
+                    if (!streamBuffer.isWritable()) {
+                        flush();
+                    }
+                    remaining -= toWrite;
+                }
+            }
+        }
+
+        @Override
+        public void flush() throws IOException {
+            checkClosed();
+
+            if (options.bodyLength() <= 0) {
+                doFlushPending(false);
+            } else {
+                doFlushPending(bytesWritten == options.bodyLength() && options.completeSendOnClose());
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (closed.compareAndSet(false, true) && !completed()) {
+                currentState = StreamState.BODY_WRITABLE;
+
+                if (options.bodyLength() > 0 && options.bodyLength() != bytesWritten) {
+                    // Limit was set but user did not write all of it so we must abort.
+                    try {
+                        abort();
+                    } catch (ClientException e) {
+                        throw new IOException(e);
+                    }
+                } else {
+                    // Limit not set or was set and user wrote that many bytes so we can complete.
+                    doFlushPending(options.completeSendOnClose());
+                }
+            }
+        }
+
+        private void checkOutputLimitReached(int writeSize) throws IOException {
+            final int outputLimit = options.bodyLength();
+
+            if (completed()) {
+                throw new IOException("Cannot write to an already completed message output stream");
+            }
+
+            if (outputLimit > 0 && (bytesWritten + writeSize) > outputLimit) {
+                throw new IOException("Cannot write beyond configured stream output limit");
+            }
+        }
+
+        private void checkClosed() throws IOException {
+            if (closed.get()) {
+                throw new IOException("The OutputStream has already been closed.");
+            }
+
+            if (sender.isClosed()) {
+                throw new IOException("The parent Sender instance has already been closed.");
+            }
+        }
+
+        protected void doFlushPending(boolean complete) throws IOException {
+            try {
+                if (streamBuffer.isReadable()) {
+                    appenedDataToBuffer(streamBuffer);
+                }
+
+                if (complete) {
+                    complete();
+                } else {
+                    doFlush();
+                }
+
+                if (!complete) {
+                    streamBuffer.setIndex(0, 0);
+                }
+            } catch (ClientException e) {
+                throw new IOException(e);
+            }
+        }
+    }
+
+    private final class SendContextRawBytesOutputStream extends StreamMessageOutputStream {
+
+        public SendContextRawBytesOutputStream(ProtonBuffer buffer) {
+            super(new OutputStreamOptions(), buffer);
+        }
+    }
+
+    private final class SingularDataSectionOutputStream extends StreamMessageOutputStream {
+
+        public SingularDataSectionOutputStream(OutputStreamOptions options, ProtonBuffer buffer) throws ClientException {
+            super(options, buffer);
+
+            ProtonBuffer preamble = ProtonByteBufferAllocator.DEFAULT.allocate(DATA_SECTION_HEADER_ENCODING_SIZE, DATA_SECTION_HEADER_ENCODING_SIZE);
+
+            preamble.writeBytes(DATA_SECTION_PREAMBLE);
+            preamble.writeInt(options.bodyLength());
+
+            appenedDataToBuffer(preamble);
+        }
+    }
+
+    private final class MultipleDataSectionsOutputStream extends StreamMessageOutputStream {
+
+        public MultipleDataSectionsOutputStream(OutputStreamOptions options, ProtonBuffer buffer) {
+            super(options, buffer);
+        }
+
+        @Override
+        protected void doFlushPending(boolean complete) throws IOException {
+            if (streamBuffer.isReadable()) {
+
+                ProtonBuffer preamble = ProtonByteBufferAllocator.DEFAULT.allocate(DATA_SECTION_HEADER_ENCODING_SIZE, DATA_SECTION_HEADER_ENCODING_SIZE);
+
+                preamble.writeBytes(DATA_SECTION_PREAMBLE);
+                preamble.writeInt(streamBuffer.getReadableBytes());
+
+                try {
+                    appenedDataToBuffer(preamble);
+                } catch (ClientException e) {
+                    throw new IOException(e);
+                }
+            }
+
+            super.doFlushPending(complete);
+        }
+    }
+
+    //----- Message Header API
+
+    @Override
+    public boolean durable() {
+        return header == null ? Header.DEFAULT_DURABILITY : header.isDurable();
+    }
+
+    @Override
+    public StreamSenderMessage durable(boolean durable) throws ClientIllegalStateException {
+        lazyCreateHeader().setDurable(durable);
+        return this;
+    }
+
+    @Override
+    public byte priority() {
+        return header == null ? Header.DEFAULT_PRIORITY : header.getPriority();
+    }
+
+    @Override
+    public StreamSenderMessage priority(byte priority) throws ClientIllegalStateException {
+        lazyCreateHeader().setPriority(priority);
+        return this;
+    }
+
+    @Override
+    public long timeToLive() {
+        return header == null ? Header.DEFAULT_TIME_TO_LIVE : header.getTimeToLive();
+    }
+
+    @Override
+    public StreamSenderMessage timeToLive(long timeToLive) throws ClientIllegalStateException {
+        lazyCreateHeader().setTimeToLive(timeToLive);
+        return this;
+    }
+
+    @Override
+    public boolean firstAcquirer() {
+        return header == null ? Header.DEFAULT_FIRST_ACQUIRER : header.isFirstAcquirer();
+    }
+
+    @Override
+    public StreamSenderMessage firstAcquirer(boolean firstAcquirer) throws ClientIllegalStateException {
+        lazyCreateHeader().setFirstAcquirer(firstAcquirer);
+        return this;
+    }
+
+    @Override
+    public long deliveryCount() {
+        return header == null ? Header.DEFAULT_DELIVERY_COUNT : header.getDeliveryCount();
+    }
+
+    @Override
+    public StreamSenderMessage deliveryCount(long deliveryCount) throws ClientIllegalStateException {
+        lazyCreateHeader().setDeliveryCount(deliveryCount);
+        return this;
+    }
+
+    //----- Message Properties access
+
+    @Override
+    public Object messageId() {
+        return properties != null ? properties.getMessageId() : null;
+    }
+
+    @Override
+    public StreamSenderMessage messageId(Object messageId) throws ClientIllegalStateException {
+        lazyCreateProperties().setMessageId(messageId);
+        return this;
+    }
+
+    @Override
+    public byte[] userId() {
+        byte[] copyOfUserId = null;
+        if (properties != null && properties.getUserId() != null) {
+            copyOfUserId = properties.getUserId().arrayCopy();
+        }
+
+        return copyOfUserId;
+    }
+
+    @Override
+    public StreamSenderMessage userId(byte[] userId) throws ClientIllegalStateException {
+        lazyCreateProperties().setUserId(new Binary(Arrays.copyOf(userId, userId.length)));
+        return this;
+    }
+
+    @Override
+    public String to() {
+        return properties != null ? properties.getTo() : null;
+    }
+
+    @Override
+    public StreamSenderMessage to(String to) throws ClientIllegalStateException {
+        lazyCreateProperties().setTo(to);
+        return this;
+    }
+
+    @Override
+    public String subject() {
+        return properties != null ? properties.getSubject() : null;
+    }
+
+    @Override
+    public StreamSenderMessage subject(String subject) throws ClientIllegalStateException {
+        lazyCreateProperties().setSubject(subject);
+        return this;
+    }
+
+    @Override
+    public String replyTo() {
+        return properties != null ? properties.getReplyTo() : null;
+    }
+
+    @Override
+    public StreamSenderMessage replyTo(String replyTo) throws ClientIllegalStateException {
+        lazyCreateProperties().setReplyTo(replyTo);
+        return this;
+    }
+
+    @Override
+    public Object correlationId() {
+        return properties != null ? properties.getCorrelationId() : null;
+    }
+
+    @Override
+    public StreamSenderMessage correlationId(Object correlationId) throws ClientIllegalStateException {
+        lazyCreateProperties().setCorrelationId(correlationId);
+        return this;
+    }
+
+    @Override
+    public String contentType() {
+        return properties != null ? properties.getContentType() : null;
+    }
+
+    @Override
+    public StreamSenderMessage contentType(String contentType) throws ClientIllegalStateException {
+        lazyCreateProperties().setContentType(contentType);
+        return this;
+    }
+
+    @Override
+    public String contentEncoding() {
+        return properties != null ? properties.getContentEncoding() : null;
+    }
+
+    @Override
+    public StreamSenderMessage contentEncoding(String contentEncoding) throws ClientIllegalStateException {
+        lazyCreateProperties().setContentEncoding(contentEncoding);
+        return this;
+    }
+
+    @Override
+    public long absoluteExpiryTime() {
+        return properties != null ? properties.getAbsoluteExpiryTime() : 0;
+    }
+
+    @Override
+    public StreamSenderMessage absoluteExpiryTime(long expiryTime) throws ClientIllegalStateException {
+        lazyCreateProperties().setAbsoluteExpiryTime(expiryTime);
+        return this;
+    }
+
+    @Override
+    public long creationTime() {
+        return properties != null ? properties.getCreationTime() : 0;
+    }
+
+    @Override
+    public StreamSenderMessage creationTime(long createTime) throws ClientIllegalStateException {
+        lazyCreateProperties().setCreationTime(createTime);
+        return this;
+    }
+
+    @Override
+    public String groupId() {
+        return properties != null ? properties.getGroupId() : null;
+    }
+
+    @Override
+    public StreamSenderMessage groupId(String groupId) throws ClientIllegalStateException {
+        lazyCreateProperties().setGroupId(groupId);
+        return this;
+    }
+
+    @Override
+    public int groupSequence() {
+        return properties != null ? (int) properties.getGroupSequence() : 0;
+    }
+
+    @Override
+    public StreamSenderMessage groupSequence(int groupSequence) throws ClientIllegalStateException {
+        lazyCreateProperties().setGroupSequence(groupSequence);
+        return this;
+    }
+
+    @Override
+    public String replyToGroupId() {
+        return properties != null ? properties.getReplyToGroupId() : null;
+    }
+
+    @Override
+    public StreamSenderMessage replyToGroupId(String replyToGroupId) throws ClientIllegalStateException {
+        lazyCreateProperties().setReplyToGroupId(replyToGroupId);
+        return this;
+    }
+
+    //----- Message Annotations Access
+
+    @Override
+    public Object annotation(String key) {
+        Object value = null;
+        if (annotations != null) {
+            value = annotations.getValue().get(Symbol.valueOf(key));
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean hasAnnotation(String key) {
+        if (annotations != null && annotations.getValue() != null) {
+            return annotations.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasAnnotations() {
+        return annotations != null &&
+               annotations.getValue() != null &&
+               annotations.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeAnnotation(String key) {
+        if (hasAnnotations()) {
+            return annotations.getValue().remove(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public StreamSenderMessage forEachAnnotation(BiConsumer<String, Object> action) {
+        if (hasAnnotations()) {
+            annotations.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientStreamSenderMessage annotation(String key, Object value) throws ClientIllegalStateException {
+        lazyCreateMessageAnnotations().getValue().put(Symbol.valueOf(key),value);
+        return this;
+    }
+
+    //----- Application Properties Access
+
+    @Override
+    public Object property(String key) {
+        Object value = null;
+        if (hasProperties()) {
+            value = applicationProperties.getValue().get(key);
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean hasProperty(String key) {
+        if (hasProperties()) {
+            return applicationProperties.getValue().containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasProperties() {
+        return applicationProperties != null &&
+               applicationProperties.getValue() != null &&
+               applicationProperties.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeProperty(String key) {
+        if (hasProperties()) {
+            return applicationProperties.getValue().remove(key);
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public StreamSenderMessage forEachProperty(BiConsumer<String, Object> action) {
+        if (hasProperties()) {
+            applicationProperties.getValue().forEach(action);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientStreamSenderMessage property(String key, Object value) throws ClientIllegalStateException {
+        lazyCreateApplicationProperties().getValue().put(key,value);
+        return this;
+    }
+
+    //----- Footer Access
+
+    @Override
+    public Object footer(String key) {
+        Object value = null;
+        if (hasFooters()) {
+            value = footer.getValue().get(Symbol.valueOf(key));
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean hasFooter(String key) {
+        if (hasFooters()) {
+            return footer.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasFooters() {
+        return footer != null &&
+               footer.getValue() != null &&
+               footer.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeFooter(String key) {
+        if (hasFooters()) {
+            return footer.getValue().remove(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public StreamSenderMessage forEachFooter(BiConsumer<String, Object> action) {
+        if (hasFooters()) {
+            footer.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public ClientStreamSenderMessage footer(String key, Object value) throws ClientIllegalStateException {
+        lazyCreateFooter().getValue().put(Symbol.valueOf(key),value);
+        return this;
+    }
+
+    //----- AdvancedMessage API
+
+    @Override
+    public Header header() throws ClientException {
+        return header;
+    }
+
+    @Override
+    public StreamSenderMessage header(Header header) throws ClientException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Header after body writing has started.");
+        this.header = header;
+        return this;
+    }
+
+    @Override
+    public MessageAnnotations annotations() throws ClientException {
+        return annotations;
+    }
+
+    @Override
+    public StreamSenderMessage annotations(MessageAnnotations annotations) throws ClientException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Annotations after body writing has started.");
+        this.annotations = annotations;
+        return this;
+    }
+
+    @Override
+    public Properties properties() throws ClientException {
+        return properties;
+    }
+
+    @Override
+    public StreamSenderMessage properties(Properties properties) throws ClientException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Properties after body writing has started.");
+        this.properties = properties;
+        return this;
+    }
+
+    @Override
+    public ApplicationProperties applicationProperties() throws ClientException {
+        return applicationProperties;
+    }
+
+    @Override
+    public StreamSenderMessage applicationProperties(ApplicationProperties applicationProperties) throws ClientException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Application Properties after body writing has started.");
+        this.applicationProperties = applicationProperties;
+        return this;
+    }
+
+    @Override
+    public Footer footer() throws ClientException {
+        return footer;
+    }
+
+    @Override
+    public StreamSenderMessage footer(Footer footer) throws ClientException {
+        if (currentState.ordinal() >= StreamState.COMPLETE.ordinal()) {
+            throw new ClientIllegalStateException(
+                "Cannot write to Message Footer after message has been marked completed or aborted.");
+        }
+        this.footer = footer;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer encode(Map<String, Object> deliveryAnnotations) throws ClientException {
+        throw new ClientUnsupportedOperationException("StreamSenderMessage cannot be directly encoded");
+    }
+
+    //----- Internal API
+
+    private void appenedDataToBuffer(ProtonBuffer incoming) throws ClientException {
+        if (buffer == null) {
+            buffer = incoming;
+        } else {
+            if (buffer instanceof ProtonCompositeBuffer) {
+                ((ProtonCompositeBuffer) buffer).append(incoming);
+            } else {
+                ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+                composite.append(buffer).append(incoming);
+
+                buffer = composite;
+            }
+        }
+
+        // Were aren't currently attempting to optimize each outbound chunk of the streaming
+        // send, if the block accumulated is larger than the write buffer we don't try and
+        // split it but instead let the frame writer just write multiple frames.  This can
+        // result in a trailing single tiny frame but for now this case isn't being optimized
+
+        if (buffer.getReadableBytes() >= writeBufferSize) {
+            try {
+                sender.sendMessage(this, streamMessagePacket);
+            } finally {
+                buffer = null;
+            }
+        }
+    }
+
+    private final class StreamMessagePacket extends ClientMessage<byte[]> {
+
+        @Override
+        public int messageFormat() {
+            return messageFormat;
+        }
+
+        @Override
+        public ProtonBuffer encode(Map<String, Object> deliveryAnnotations) {
+            return buffer;
+        }
+    }
+
+    private void transitionToWritableState() throws ClientException {
+        if (currentState == StreamState.PREAMBLE) {
+
+            if (header != null) {
+                appenedDataToBuffer(ClientMessageSupport.encodeSection(header, ProtonByteBufferAllocator.DEFAULT.allocate()));
+            }
+            if (deliveryAnnotations != null) {
+                appenedDataToBuffer(ClientMessageSupport.encodeSection(deliveryAnnotations, ProtonByteBufferAllocator.DEFAULT.allocate()));
+            }
+            if (annotations != null) {
+                appenedDataToBuffer(ClientMessageSupport.encodeSection(annotations, ProtonByteBufferAllocator.DEFAULT.allocate()));
+            }
+            if (properties != null) {
+                appenedDataToBuffer(ClientMessageSupport.encodeSection(properties, ProtonByteBufferAllocator.DEFAULT.allocate()));
+            }
+            if (applicationProperties != null) {
+                appenedDataToBuffer(ClientMessageSupport.encodeSection(applicationProperties, ProtonByteBufferAllocator.DEFAULT.allocate()));
+            }
+
+            currentState = StreamState.BODY_WRITABLE;
+        }
+    }
+
+    private ClientStreamSenderMessage write(Section<?> section) throws ClientException {
+        if (aborted()) {
+            throw new ClientIllegalStateException("Cannot write a Section to an already aborted send context");
+        }
+
+        if (completed()) {
+            throw new ClientIllegalStateException("Cannot write a Section to an already completed send context");
+        }
+
+        appenedDataToBuffer(ClientMessageSupport.encodeSection(section, ProtonByteBufferAllocator.DEFAULT.allocate()));
+
+        return this;
+    }
+
+    private void checkStreamState(StreamState state, String errorMessage) throws ClientIllegalStateException {
+        if (currentState != state) {
+            throw new ClientIllegalStateException(errorMessage);
+        }
+    }
+
+    private Header lazyCreateHeader() throws ClientIllegalStateException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Header after body writing has started.");
+
+        if (header == null) {
+            header = new Header();
+        }
+
+        return header;
+    }
+
+    private Properties lazyCreateProperties() throws ClientIllegalStateException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Properties after body writing has started.");
+
+        if (properties == null) {
+            properties = new Properties();
+        }
+
+        return properties;
+    }
+
+    private ApplicationProperties lazyCreateApplicationProperties() throws ClientIllegalStateException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Application Properties after body writing has started.");
+
+        if (applicationProperties == null) {
+            applicationProperties = new ApplicationProperties(new LinkedHashMap<>());
+        }
+
+        return applicationProperties;
+    }
+
+    private MessageAnnotations lazyCreateMessageAnnotations() throws ClientIllegalStateException {
+        checkStreamState(StreamState.PREAMBLE, "Cannot write to Message Annotations after body writing has started.");
+
+        if (annotations == null) {
+            annotations = new MessageAnnotations(new LinkedHashMap<>());
+        }
+
+        return annotations;
+    }
+
+    private Footer lazyCreateFooter() throws ClientIllegalStateException {
+        if (currentState.ordinal() >= StreamState.COMPLETE.ordinal()) {
+            throw new ClientIllegalStateException(
+                "Cannot write to Message Footer after message has been marked completed or aborted.");
+        }
+
+        if (footer == null) {
+            footer = new Footer(new LinkedHashMap<>());
+        }
+
+        return footer;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSession.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSession.java
new file mode 100644
index 0000000..1ab77ae
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamSession.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.engine.Session;
+
+/**
+ * A specialized {@link ClientSession} that is the parent of a {@link ClientStreamSender} or
+ * {@link ClientStreamReceiver} and cannot create any further resources as the lifetime of the
+ * session is tied to the child {@link StreamSender} or {@link StreamReceiver}.
+ */
+public final class ClientStreamSession extends ClientSession {
+
+    public ClientStreamSession(ClientConnection connection, SessionOptions options, String sessionId, Session session) {
+        super(connection, options, sessionId, session);
+    }
+
+    @Override
+    public Receiver openReceiver(String address) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openReceiver(String address, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openDurableReceiver(String address, String subscriptionName) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openDurableReceiver(String address, String subscriptionName, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openDynamicReceiver() throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Receiver openDynamicReceiver(Map<String, Object> dynamicNodeProperties, ReceiverOptions receiverOptions) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Sender openSender(String address) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Sender openSender(String address, SenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Sender openAnonymousSender() throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+
+    @Override
+    public Sender openAnonymousSender(SenderOptions senderOptions) throws ClientException {
+        checkClosedOrFailed();
+        throw new ClientUnsupportedOperationException("Cannot create a receiver from a streaming resource session");
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamTracker.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamTracker.java
new file mode 100644
index 0000000..b9b2399
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientStreamTracker.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamTracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+
+/**
+ * {@link StreamTracker} implementation that relies on the ClientTracker to handle the
+ * basic {@link OutgoingDelivery} management.
+ */
+public final class ClientStreamTracker extends ClientTracker implements StreamTracker {
+
+    public ClientStreamTracker(ClientStreamSender sender, OutgoingDelivery delivery) {
+        super(sender, delivery);
+    }
+
+    @Override
+    public StreamSender sender() {
+        return (StreamSender) super.sender();
+    }
+
+    @Override
+    public StreamTracker disposition(DeliveryState state, boolean settle) throws ClientException {
+        return (StreamTracker) super.disposition(state, settle);
+    }
+
+    @Override
+    public StreamTracker settle() throws ClientException {
+        return (StreamTracker) super.settle();
+    }
+
+    @Override
+    public StreamTracker awaitSettlement() throws ClientException {
+        return (StreamTracker) super.awaitSettlement();
+    }
+
+    @Override
+    public StreamTracker awaitSettlement(long timeout, TimeUnit unit) throws ClientException {
+        return (StreamTracker) super.awaitSettlement(timeout, unit);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTracker.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTracker.java
new file mode 100644
index 0000000..52ddf18
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTracker.java
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientDeliveryStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+
+/**
+ * Client outgoing delivery tracker object.
+ */
+class ClientTracker implements Tracker {
+
+    private final ClientSender sender;
+    private final OutgoingDelivery delivery;
+
+    private final ClientFuture<Tracker> remoteSettlementFuture;
+
+    private volatile boolean remotelySetted;
+    private volatile DeliveryState remoteDeliveryState;
+
+    /**
+     * Create an instance of a client outgoing delivery tracker.
+     *
+     * @param sender
+     *      The sender that was used to send the delivery
+     * @param delivery
+     *      The proton outgoing delivery object that backs this tracker.
+     */
+    ClientTracker(ClientSender sender, OutgoingDelivery delivery) {
+        this.sender = sender;
+        this.delivery = delivery;
+        this.delivery.deliveryStateUpdatedHandler(this::processDeliveryUpdated);
+        this.remoteSettlementFuture = sender.session().getFutureFactory().createFuture();
+    }
+
+    OutgoingDelivery delivery() {
+        return delivery;
+    }
+
+    @Override
+    public Sender sender() {
+        return sender;
+    }
+
+    @Override
+    public synchronized DeliveryState state() {
+        return ClientDeliveryState.fromProtonType(delivery.getState());
+    }
+
+    @Override
+    public DeliveryState remoteState() {
+        return remoteDeliveryState;
+    }
+
+    @Override
+    public boolean remoteSettled() {
+        return remotelySetted;
+    }
+
+    @Override
+    public Tracker disposition(DeliveryState state, boolean settle) throws ClientException {
+        try {
+            sender.disposition(delivery, ClientDeliveryState.asProtonType(state), settle);
+        } finally {
+            if (settle) {
+                remoteSettlementFuture.complete(this);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public Tracker settle() throws ClientException {
+        try {
+            sender.disposition(delivery, null, true);
+        } finally {
+            remoteSettlementFuture.complete(this);
+        }
+
+        return this;
+    }
+
+    @Override
+    public synchronized boolean settled() {
+        return delivery.isSettled();
+    }
+
+    @Override
+    public ClientFuture<Tracker> settlementFuture() {
+        if (delivery.isSettled()) {
+            remoteSettlementFuture.complete(this);
+        }
+
+        return remoteSettlementFuture;
+    }
+
+    @Override
+    public Tracker awaitSettlement() throws ClientException {
+        try {
+            if (settled()) {
+                return this;
+            } else {
+                return settlementFuture().get();
+            }
+        } catch (ExecutionException exe) {
+            throw ClientExceptionSupport.createNonFatalOrPassthrough(exe.getCause());
+        } catch (InterruptedException e) {
+            Thread.interrupted();
+            throw new ClientException("Wait for settlement was interrupted", e);
+        }
+    }
+
+    @Override
+    public Tracker awaitSettlement(long timeout, TimeUnit unit) throws ClientException {
+        try {
+            if (settled()) {
+                return this;
+            } else {
+                return settlementFuture().get(timeout, unit);
+            }
+        } catch (InterruptedException ie) {
+            Thread.interrupted();
+            throw new ClientException("Wait for settlement was interrupted", ie);
+        } catch (ExecutionException exe) {
+            throw ClientExceptionSupport.createNonFatalOrPassthrough(exe.getCause());
+        } catch (TimeoutException te) {
+            throw new ClientOperationTimedOutException("Timed out waiting for remote settlement", te);
+        }
+    }
+
+    @Override
+    public Tracker awaitAccepted() throws ClientException {
+        try {
+            if (settled() && !remoteSettled()) {
+                return this;
+            } else {
+                settlementFuture().get();
+                if (remoteState() != null && remoteState().isAccepted()) {
+                    return this;
+                } else {
+                    throw new ClientDeliveryStateException("Remote did not accept the sent message", remoteState());
+                }
+            }
+        } catch (ExecutionException exe) {
+            throw ClientExceptionSupport.createNonFatalOrPassthrough(exe.getCause());
+        } catch (InterruptedException ie) {
+            Thread.interrupted();
+            throw new ClientException("Wait for Accepted outcome was interrupted", ie);
+        }
+    }
+
+    @Override
+    public Tracker awaitAccepted(long timeout, TimeUnit unit) throws ClientException {
+        try {
+            if (settled() && !remoteSettled()) {
+                return this;
+            } else {
+                settlementFuture().get(timeout, unit);
+                if (remoteState() != null && remoteState().isAccepted()) {
+                    return this;
+                } else {
+                    throw new ClientDeliveryStateException("Remote did not accept the sent message", remoteState());
+                }
+            }
+        } catch (InterruptedException ie) {
+            Thread.interrupted();
+            throw new ClientException("Wait for Accepted outcome was interrupted", ie);
+        } catch (ExecutionException exe) {
+            throw ClientExceptionSupport.createNonFatalOrPassthrough(exe.getCause());
+        } catch (TimeoutException te) {
+            throw new ClientOperationTimedOutException("Timed out waiting for remote Accepted outcome", te);
+        }
+    }
+
+    //----- Internal Event hooks for delivery updates
+
+    private void processDeliveryUpdated(OutgoingDelivery delivery) {
+        remotelySetted = delivery.isRemotelySettled();
+        remoteDeliveryState = ClientDeliveryState.fromProtonType(delivery.getRemoteState());
+
+        if (delivery.isRemotelySettled()) {
+            remoteSettlementFuture.complete(this);
+        }
+
+        if (sender.options().autoSettle() && delivery.isRemotelySettled()) {
+            delivery.settle();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTransactionContext.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTransactionContext.java
new file mode 100644
index 0000000..b0af3de
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTransactionContext.java
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.futures.ClientFuture;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+/**
+ * Base for a Transaction Context used in {@link ClientSession} instances
+ * to mask from the senders and receivers the work of deciding transaction
+ * specific behaviors.
+ */
+public interface ClientTransactionContext {
+
+    /**
+     * Begin a new transaction if one is not already in play.
+     *
+     * @param beginFuture
+     *      The future that awaits the result of starting the new transaction.
+     *
+     * @return this {@link ClientTransactionContext} instance.
+     *
+     * @throws ClientIllegalStateException if an error occurs do to the transaction state.
+     */
+    ClientTransactionContext begin(ClientFuture<Session> beginFuture) throws ClientIllegalStateException;
+
+    /**
+     * Commits the current transaction if one is active and is not failed into a roll-back only
+     * state.
+     *
+     * @param commitFuture
+     *      The future that awaits the result of committing the new transaction.
+     * @param startNew
+     *      Should the context immediately initiate a new transaction
+     *
+     * @return this {@@Override
+    link ClientTransactionContext} instance.
+     *
+     * @throws ClientIllegalStateException if an error occurs do to the transaction state.
+     */
+    ClientTransactionContext commit(ClientFuture<Session> commitFuture, boolean startNew) throws ClientIllegalStateException;
+
+    /**
+     * Rolls back the current transaction if one is active.
+     *
+     * @param rollbackFuture
+     *      The future that awaits the result of rolling back the new transaction.
+     * @param startNew
+     *      Should the context immediately initiate a new transaction
+     *
+     * @return this {@link ClientTransactionContext} instance.
+     *
+     * @throws ClientIllegalStateException if an error occurs do to the transaction state.
+     */
+    ClientTransactionContext rollback(ClientFuture<Session> rollbackFuture, boolean startNew) throws ClientIllegalStateException;
+
+    /**
+     * @return true if the context is hosting an active transaction.
+     */
+    boolean isInTransaction();
+
+    /**
+     * @return true if there is an active transaction but its state is failed an will roll-back
+     */
+    boolean isRollbackOnly();
+
+    /**
+     * Enlist the given outgoing envelope into this transaction if one is active and not already
+     * in a roll-back only state.  If the transaction is failed the context should discard the
+     * envelope which should appear to the caller as if the send was successful.
+     *
+     * @param envelope
+     *      The envelope containing the details and mechanisms for sending the message.
+     * @param state
+     *      The delivery state that is being applied as the outcome of the delivery.
+     * @param settled
+     *      The settlement value that is being requested for the delivery.
+     *
+     * @return this {@link ClientTransactionContext} instance.
+     */
+    ClientTransactionContext send(ClientOutgoingEnvelope envelope, DeliveryState state, boolean settled);
+
+    /**
+     * Apply a disposition to the given delivery wrapping it with a {@link TransactionalState} outcome
+     * if there is an active transaction.  If there is no active transaction than the context will apply
+     * the disposition as requested but if there is an active transaction then the disposition must be
+     * wrapped in a {@link TransactionalState} and settlement should always enforced by the client.
+     *
+     * @param delivery
+     *      The incoming delivery that the receiver is applying a disposition to.
+     * @param state
+     *      The delivery state that is being applied as the outcome of the delivery.
+     * @param settled
+     *      The settlement value that is being requested for the delivery.
+     *
+     * @return this {@link ClientTransactionContext} instance.
+     */
+    ClientTransactionContext disposition(IncomingDelivery delivery, DeliveryState state, boolean settled);
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTransportListener.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTransportListener.java
new file mode 100644
index 0000000..58b0a43
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientTransportListener.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.transport.Transport;
+import org.apache.qpid.protonj2.client.transport.TransportListener;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Transport events listener that is bound to a single proton {@link Engine} instance
+ * for its lifetime which prevent duplication of error or connection closed events from
+ * influencing a {@link ClientConnection} that will attempt reconnection.
+ */
+final class ClientTransportListener implements TransportListener {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientTransportListener.class);
+
+    private final Engine engine;
+
+    public ClientTransportListener(Engine engine) {
+        this.engine = engine;
+    }
+
+    @Override
+    public void transportInitialized(Transport transport) {
+        engine.configuration().setBufferAllocator(transport.getBufferAllocator());
+    }
+
+    @Override
+    public void transportConnected(Transport transport) {
+        engine.start().open();
+    }
+
+    @Override
+    public void transportRead(ProtonBuffer incoming) {
+        try {
+            do {
+                engine.ingest(incoming);
+            } while (incoming.isReadable() && engine.isWritable());
+            // TODO - How do we handle case of not all data read ?
+        } catch (EngineStateException e) {
+            LOG.warn("Caught problem during incoming data processing: {}", e.getMessage(), e);
+            engine.engineFailed(ClientExceptionSupport.createOrPassthroughFatal(e));
+        }
+    }
+
+    @Override
+    public void transportError(Throwable error) {
+        if (!engine.isShutdown()) {
+            LOG.debug("Transport failed: {}", error.getMessage());
+            engine.engineFailed(ClientExceptionSupport.convertToConnectionClosedException(error));
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/EpollSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/EpollSupport.java
new file mode 100644
index 0000000..229ec7d
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/EpollSupport.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.util.concurrent.ThreadFactory;
+
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.epoll.Epoll;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.epoll.EpollSocketChannel;
+
+public class EpollSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(EpollSupport.class);
+
+    public static final String NAME = "EPOLL";
+
+    public static boolean isAvailable(TransportOptions transportOptions) {
+        try {
+            return transportOptions.allowNativeIO() && Epoll.isAvailable();
+        } catch (NoClassDefFoundError ncdfe) {
+            LOG.debug("Unable to check for Epoll support due to missing class definition", ncdfe);
+            return false;
+        }
+    }
+
+    public static EventLoopGroup createGroup(int nThreads, ThreadFactory ioThreadfactory) {
+        return new EpollEventLoopGroup(nThreads, ioThreadfactory);
+    }
+
+    public static Class<? extends Channel> getChannelClass() {
+        return EpollSocketChannel.class;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/IOUringSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/IOUringSupport.java
new file mode 100644
index 0000000..c7e8aa4
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/IOUringSupport.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.util.concurrent.ThreadFactory;
+
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.incubator.channel.uring.IOUring;
+import io.netty.incubator.channel.uring.IOUringEventLoopGroup;
+import io.netty.incubator.channel.uring.IOUringSocketChannel;
+
+public class IOUringSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(IOUringSupport.class);
+
+    public static final String NAME = "IO_URING";
+
+    public static boolean isAvailable(TransportOptions transportOptions) {
+        try {
+            return transportOptions.allowNativeIO() && IOUring.isAvailable();
+        } catch (NoClassDefFoundError ncdfe) {
+            LOG.debug("Unable to check for IO_Uring support due to missing class definition", ncdfe);
+            return false;
+        }
+    }
+
+    public static EventLoopGroup createGroup(int nThreads, ThreadFactory ioThreadfactory) {
+        return new IOUringEventLoopGroup(nThreads, ioThreadfactory);
+    }
+
+    public static Class<? extends Channel> getChannelClass() {
+        return IOUringSocketChannel.class;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/KQueueSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/KQueueSupport.java
new file mode 100644
index 0000000..2567e6f
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/KQueueSupport.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.util.concurrent.ThreadFactory;
+
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.kqueue.KQueue;
+import io.netty.channel.kqueue.KQueueEventLoopGroup;
+import io.netty.channel.kqueue.KQueueSocketChannel;
+
+public class KQueueSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(KQueueSupport.class);
+
+    public static final String NAME = "KQUEUE";
+
+    public static boolean isAvailable(TransportOptions transportOptions) {
+        try {
+            return transportOptions.allowNativeIO() && KQueue.isAvailable();
+        } catch (NoClassDefFoundError ncdfe) {
+            LOG.debug("Unable to check for KQueue support due to missing class definition", ncdfe);
+            return false;
+        }
+    }
+
+    public static EventLoopGroup createGroup(int nThreads, ThreadFactory ioThreadfactory) {
+        return new KQueueEventLoopGroup(nThreads, ioThreadfactory);
+    }
+
+    public static Class<? extends Channel> getChannelClass() {
+        return KQueueSocketChannel.class;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/NettyIOContext.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/NettyIOContext.java
new file mode 100644
index 0000000..b52b5da
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/NettyIOContext.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.util.Objects;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.apache.qpid.protonj2.client.util.TrackableThreadFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.util.concurrent.Future;
+
+/**
+ * Builder of Transport instances that will validate the build options and produce a
+ * correctly configured transport based on the options set.
+ */
+public final class NettyIOContext {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NettyIOContext.class);
+
+    private static final int SHUTDOWN_TIMEOUT = 50;
+
+    private final EventLoopGroup group;
+    private final Class<? extends Channel> channelClass;
+    private final TransportOptions options;
+    private final SslOptions sslOptions;
+    private final ThreadFactory threadFactory;
+
+    public NettyIOContext(TransportOptions options, SslOptions ssl, String ioThreadName) {
+        Objects.requireNonNull(options, "Transport Options cannot be null");
+        Objects.requireNonNull(ssl, "Transport SSL Options cannot be null");
+
+        this.options = options;
+        this.sslOptions = ssl;
+        this.threadFactory = new TrackableThreadFactory(ioThreadName, true);
+
+        final String[] nativeIOPreference = options.nativeIOPeference();
+
+        EventLoopGroup selectedGroup = null;
+        Class<? extends Channel> selectedChannelClass = null;
+
+        if (options.allowNativeIO()) {
+            for (String nativeID : nativeIOPreference) {
+                if (EpollSupport.NAME.equalsIgnoreCase(nativeID) && EpollSupport.isAvailable(options)) {
+                    LOG.trace("Netty Transports will be using Epoll mode");
+                    selectedGroup = EpollSupport.createGroup(1, threadFactory);
+                    selectedChannelClass = EpollSupport.getChannelClass();
+                    break;
+                } else if (IOUringSupport.NAME.equalsIgnoreCase(nativeID) && IOUringSupport.isAvailable(options)) {
+                    LOG.trace("Netty Transports will be using IO-Uring mode");
+                    selectedGroup = IOUringSupport.createGroup(1, threadFactory);
+                    selectedChannelClass = IOUringSupport.getChannelClass();
+                    break;
+                } else if (KQueueSupport.NAME.equalsIgnoreCase(nativeID) && KQueueSupport.isAvailable(options)) {
+                    LOG.trace("Netty Transports will be using KQueue mode");
+                    selectedGroup = KQueueSupport.createGroup(1, threadFactory);
+                    selectedChannelClass = KQueueSupport.getChannelClass();
+                    break;
+                } else {
+                    throw new IllegalArgumentException(
+                        String.format("Provided preferred native trasport type name: %s , is not vliad.", nativeID));
+                }
+            }
+        }
+
+        if (selectedGroup == null) {
+            LOG.trace("Netty Transports will be using NIO mode");
+            selectedGroup = new NioEventLoopGroup(1, threadFactory);
+            selectedChannelClass = NioSocketChannel.class;
+        }
+
+        this.group = selectedGroup;
+        this.channelClass = selectedChannelClass;
+    }
+
+    public void shutdown() {
+        if (!group.isShutdown()) {
+            Future<?> fut = group.shutdownGracefully(0, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
+            if (!fut.awaitUninterruptibly(2 * SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS)) {
+                LOG.trace("Connection IO Event Loop shutdown failed to complete in allotted time");
+            }
+        }
+    }
+
+    public EventLoopGroup eventLoop() {
+        return group;
+    }
+
+    public TcpTransport newTransport() {
+        if (group.isShutdown() || group.isShuttingDown() || group.isTerminated()) {
+            throw new IllegalStateException("Cannot create a Transport from a shutdown IO context");
+        }
+
+        final Bootstrap bootstrap = new Bootstrap().channel(channelClass).group(group);
+
+        final TcpTransport transport;
+
+        if (options.useWebSockets()) {
+            transport = new WebSocketTransport(bootstrap, options, sslOptions);
+        } else {
+            transport = new TcpTransport(bootstrap, options, sslOptions);
+        }
+
+        return transport;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/SslSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/SslSupport.java
new file mode 100644
index 0000000..7a2c422
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/SslSupport.java
@@ -0,0 +1,446 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.OpenSslX509KeyManagerFactory;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+
+/**
+ * Static class that provides various utility methods used by Transport implementations.
+ */
+public class SslSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SslSupport.class);
+
+    /**
+     * Determines if Netty OpenSSL support is available and applicable based on the configuration
+     * in the given TransportOptions instance.
+     *
+     * @param options
+     * 		  The configuration of the Transport being created.
+     *
+     * @return true if OpenSSL support is available and usable given the requested configuration.
+     */
+    public static boolean isOpenSSLPossible(SslOptions options) {
+        boolean result = false;
+
+        if (options.allowNativeSSL()) {
+            if (!OpenSsl.isAvailable()) {
+                LOG.debug("OpenSSL could not be enabled because a suitable implementation could not be found.", OpenSsl.unavailabilityCause());
+            } else if (options.sslContextOverride() != null) {
+                LOG.debug("OpenSSL could not be enabled due to user SSLContext being supplied.");
+            } else if (!OpenSsl.supportsKeyManagerFactory()) {
+                LOG.debug("OpenSSL could not be enabled because the version provided does not allow a KeyManagerFactory to be used.");
+            } else if (options.keyAlias() != null) {
+                LOG.debug("OpenSSL could not be enabled because a keyAlias is set and that feature is not supported for OpenSSL.");
+            } else {
+                LOG.debug("OpenSSL Enabled: Version {} of OpenSSL will be used", OpenSsl.versionString());
+                result = true;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Creates a Netty SslHandler instance for use in Transports that require
+     * an SSL encoder / decoder.
+     *
+     * If the given options contain an SSLContext override, this will be used directly
+     * when creating the handler. If they do not, an SSLContext will first be created
+     * using the other option values.
+     *
+     * @param allocator
+     *		  The Netty Buffer Allocator to use when Netty resources need to be created.
+     * @param host
+     *        the host name or IP address that this transport connects to.
+     * @param port
+     * 		  the port on the given host that this transport connects to.
+     * @param options
+     *        The SSL options object to build the SslHandler instance from.
+     *
+     * @return a new SslHandler that is configured from the given options.
+     *
+     * @throws Exception if an error occurs while creating the SslHandler instance.
+     */
+    public static SslHandler createSslHandler(ByteBufAllocator allocator, String host, int port, SslOptions options) throws Exception {
+        final SSLEngine sslEngine;
+
+        if (isOpenSSLPossible(options)) {
+            SslContext sslContext = createOpenSslContext(options);
+            sslEngine = createOpenSslEngine(allocator, host, port, sslContext, options);
+        } else {
+            SSLContext sslContext = options.sslContextOverride();
+            if (sslContext == null) {
+                sslContext = createJdkSslContext(options);
+            }
+
+            sslEngine = createJdkSslEngine(host, port, sslContext, options);
+        }
+
+        return new SslHandler(sslEngine);
+    }
+
+    //----- JDK SSL Support Methods ------------------------------------------//
+
+    /**
+     * Create a new SSLContext using the options specific in the given TransportOptions
+     * instance.
+     *
+     * @param options
+     *        the configured options used to create the SSLContext.
+     *
+     * @return a new SSLContext instance.
+     *
+     * @throws Exception if an error occurs while creating the context.
+     */
+    public static SSLContext createJdkSslContext(SslOptions options) throws Exception {
+        try {
+            String contextProtocol = options.contextProtocol();
+            LOG.trace("Getting SSLContext instance using protocol: {}", contextProtocol);
+
+            SSLContext context = SSLContext.getInstance(contextProtocol);
+
+            KeyManager[] keyMgrs = loadKeyManagers(options);
+            TrustManager[] trustManagers = loadTrustManagers(options);
+
+            context.init(keyMgrs, trustManagers, new SecureRandom());
+            return context;
+        } catch (Exception e) {
+            LOG.error("Failed to create SSLContext: {}", e, e);
+            throw e;
+        }
+    }
+
+    /**
+     * Create a new JDK SSLEngine instance in client mode from the given SSLContext and
+     * TransportOptions instances.
+     *
+     * @param host
+     *        the host name or IP address that this transport connects to.
+     * @param port
+     * 		  the port on the given host that this transport connects to.
+     * @param context
+     *        the SSLContext to use when creating the engine.
+     * @param options
+     *        the TransportOptions to use to configure the new SSLEngine.
+     *
+     * @return a new SSLEngine instance in client mode.
+     *
+     * @throws Exception if an error occurs while creating the new SSLEngine.
+     */
+    public static SSLEngine createJdkSslEngine(String host, int port, SSLContext context, SslOptions options) throws Exception {
+        SSLEngine engine = null;
+        if (host == null || host.isEmpty()) {
+            engine = context.createSSLEngine();
+        } else {
+            engine = context.createSSLEngine(host, port);
+        }
+
+        engine.setEnabledProtocols(buildEnabledProtocols(engine, options));
+        engine.setEnabledCipherSuites(buildEnabledCipherSuites(engine, options));
+        engine.setUseClientMode(true);
+
+        if (options.verifyHost()) {
+            SSLParameters sslParameters = engine.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            engine.setSSLParameters(sslParameters);
+        }
+
+        return engine;
+    }
+
+    //----- OpenSSL Support Methods ------------------------------------------//
+
+    /**
+     * Create a new Netty SslContext using the options specific in the given TransportOptions
+     * instance.
+     *
+     * @param options
+     *        the configured options used to create the SslContext.
+     *
+     * @return a new SslContext instance.
+     *
+     * @throws Exception if an error occurs while creating the context.
+     */
+    public static SslContext createOpenSslContext(SslOptions options) throws Exception {
+        try {
+            String contextProtocol = options.contextProtocol();
+            LOG.trace("Getting SslContext instance using protocol: {}", contextProtocol);
+
+            KeyManagerFactory keyManagerFactory = loadKeyManagerFactory(options, SslProvider.OPENSSL);
+            TrustManagerFactory trustManagerFactory = loadTrustManagerFactory(options);
+            SslContextBuilder builder = SslContextBuilder.forClient().sslProvider(SslProvider.OPENSSL);
+
+            // TODO - There is oddly no way in Netty right now to get the set of supported protocols
+            //        when creating the SslContext or really even when creating the SSLEngine.  Seems
+            //        like an oversight, for now we call it with TLSv1.2 so it looks like we did something.
+            if (options.contextProtocol().equals(SslOptions.DEFAULT_CONTEXT_PROTOCOL)) {
+                builder.protocols("TLSv1.2");
+            } else {
+                builder.protocols(options.contextProtocol());
+            }
+            builder.keyManager(keyManagerFactory);
+            builder.trustManager(trustManagerFactory);
+
+            return builder.build();
+        } catch (Exception e) {
+            LOG.error("Failed to create SslContext: {}", e, e);
+            throw e;
+        }
+    }
+
+    /**
+     * Create a new OpenSSL SSLEngine instance in client mode from the given SSLContext and
+     * TransportOptions instances.
+     *
+     * @param allocator
+     *		  the Netty ByteBufAllocator to use to create the OpenSSL engine
+     * @param host
+     *        the host name or IP address that this transport connects to.
+     * @param port
+     * 		  the port on the given host that this transport connects to.
+     * @param context
+     *        the Netty SslContext to use when creating the engine.
+     * @param options
+     *        the TransportOptions to use to configure the new SSLEngine.
+     *
+     * @return a new Netty managed SSLEngine instance in client mode.
+     *
+     * @throws Exception if an error occurs while creating the new SSLEngine.
+     */
+    public static SSLEngine createOpenSslEngine(ByteBufAllocator allocator, String host, int port, SslContext context, SslOptions options) throws Exception {
+        SSLEngine engine = null;
+
+        if (allocator == null) {
+            throw new IllegalArgumentException("OpenSSL engine requires a valid ByteBufAllocator to operate");
+        }
+
+        if (host == null || host.isEmpty()) {
+            engine = context.newEngine(allocator);
+        } else {
+            engine = context.newEngine(allocator, host, port);
+        }
+
+        engine.setEnabledProtocols(buildEnabledProtocols(engine, options));
+        engine.setEnabledCipherSuites(buildEnabledCipherSuites(engine, options));
+        engine.setUseClientMode(true);
+
+        if (options.verifyHost()) {
+            SSLParameters sslParameters = engine.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            engine.setSSLParameters(sslParameters);
+        }
+
+        return engine;
+    }
+
+    //----- Internal support methods -----------------------------------------//
+
+    private static String[] buildEnabledProtocols(SSLEngine engine, SslOptions options) {
+        List<String> enabledProtocols = new ArrayList<String>();
+
+        if (options.enabledProtocols() != null) {
+            List<String> configuredProtocols = Arrays.asList(options.enabledProtocols());
+            LOG.trace("Configured protocols from transport options: {}", configuredProtocols);
+            enabledProtocols.addAll(configuredProtocols);
+        } else {
+            List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+            LOG.trace("Default protocols from the SSLEngine: {}", engineProtocols);
+            enabledProtocols.addAll(engineProtocols);
+        }
+
+        String[] disabledProtocols = options.disabledProtocols();
+        if (disabledProtocols != null) {
+            List<String> disabled = Arrays.asList(disabledProtocols);
+            LOG.trace("Disabled protocols: {}", disabled);
+            enabledProtocols.removeAll(disabled);
+        }
+
+        LOG.trace("Enabled protocols: {}", enabledProtocols);
+
+        return enabledProtocols.toArray(new String[0]);
+    }
+
+    private static String[] buildEnabledCipherSuites(SSLEngine engine, SslOptions options) {
+        List<String> enabledCipherSuites = new ArrayList<String>();
+
+        if (options.enabledCipherSuites() != null) {
+            List<String> configuredCipherSuites = Arrays.asList(options.enabledCipherSuites());
+            LOG.trace("Configured cipher suites from transport options: {}", configuredCipherSuites);
+            enabledCipherSuites.addAll(configuredCipherSuites);
+        } else {
+            List<String> engineCipherSuites = Arrays.asList(engine.getEnabledCipherSuites());
+            LOG.trace("Default cipher suites from the SSLEngine: {}", engineCipherSuites);
+            enabledCipherSuites.addAll(engineCipherSuites);
+        }
+
+        String[] disabledCipherSuites = options.disabledCipherSuites();
+        if (disabledCipherSuites != null) {
+            List<String> disabled = Arrays.asList(disabledCipherSuites);
+            LOG.trace("Disabled cipher suites: {}", disabled);
+            enabledCipherSuites.removeAll(disabled);
+        }
+
+        LOG.trace("Enabled cipher suites: {}", enabledCipherSuites);
+
+        return enabledCipherSuites.toArray(new String[0]);
+    }
+
+    private static TrustManager[] loadTrustManagers(SslOptions options) throws Exception {
+        TrustManagerFactory factory = loadTrustManagerFactory(options);
+        if (factory != null) {
+            return factory.getTrustManagers();
+        } else {
+            return null;
+        }
+    }
+
+    private static TrustManagerFactory loadTrustManagerFactory(SslOptions options) throws Exception {
+        if (options.trustAll()) {
+            return InsecureTrustManagerFactory.INSTANCE;
+        }
+
+        if (options.trustStoreLocation() == null) {
+            return null;
+        }
+
+        TrustManagerFactory fact = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+        String storeLocation = options.trustStoreLocation();
+        String storePassword = options.trustStorePassword();
+        String storeType = options.trustStoreType();
+
+        LOG.trace("Attempt to load TrustStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore trustStore = loadStore(storeLocation, storePassword, storeType);
+        fact.init(trustStore);
+
+        return fact;
+    }
+
+    private static KeyManager[] loadKeyManagers(SslOptions options) throws Exception {
+        if (options.keyStoreLocation() == null) {
+            return null;
+        }
+
+        KeyManagerFactory fact = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+        String storeLocation = options.keyStoreLocation();
+        String storePassword = options.keyStorePassword();
+        String storeType = options.keyStoreType();
+        String alias = options.keyAlias();
+
+        LOG.trace("Attempt to load KeyStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore keyStore = loadStore(storeLocation, storePassword, storeType);
+        fact.init(keyStore, storePassword != null ? storePassword.toCharArray() : null);
+
+        if (alias == null) {
+            return fact.getKeyManagers();
+        } else {
+            validateAlias(keyStore, alias);
+            return wrapKeyManagers(alias, fact.getKeyManagers());
+        }
+    }
+
+    private static KeyManagerFactory loadKeyManagerFactory(SslOptions options, SslProvider provider) throws Exception {
+        if (options.keyStoreLocation() == null) {
+            return null;
+        }
+
+        final KeyManagerFactory factory;
+        if (provider.equals(SslProvider.JDK)) {
+            factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        } else {
+            factory = new OpenSslX509KeyManagerFactory();
+        }
+
+        String storeLocation = options.keyStoreLocation();
+        String storePassword = options.keyStorePassword();
+        String storeType = options.keyStoreType();
+
+        LOG.trace("Attempt to load KeyStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore keyStore = loadStore(storeLocation, storePassword, storeType);
+        factory.init(keyStore, storePassword != null ? storePassword.toCharArray() : null);
+
+        return factory;
+    }
+
+    private static KeyManager[] wrapKeyManagers(String alias, KeyManager[] origKeyManagers) {
+        KeyManager[] keyManagers = new KeyManager[origKeyManagers.length];
+        for (int i = 0; i < origKeyManagers.length; i++) {
+            KeyManager km = origKeyManagers[i];
+            if (km instanceof X509ExtendedKeyManager) {
+                km = new X509AliasKeyManager(alias, (X509ExtendedKeyManager) km);
+            }
+
+            keyManagers[i] = km;
+        }
+
+        return keyManagers;
+    }
+
+    private static void validateAlias(KeyStore store, String alias) throws IllegalArgumentException, KeyStoreException {
+        if (!store.containsAlias(alias)) {
+            throw new IllegalArgumentException("The alias '" + alias + "' doesn't exist in the key store");
+        }
+
+        if (!store.isKeyEntry(alias)) {
+            throw new IllegalArgumentException("The alias '" + alias + "' in the keystore doesn't represent a key entry");
+        }
+    }
+
+    private static KeyStore loadStore(String storePath, final String password, String storeType) throws Exception {
+        KeyStore store = KeyStore.getInstance(storeType);
+        try (InputStream in = new FileInputStream(new File(storePath));) {
+            store.load(in, password != null ? password.toCharArray() : null);
+        }
+
+        return store;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/TcpTransport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/TcpTransport.java
new file mode 100644
index 0000000..a179075
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/TcpTransport.java
@@ -0,0 +1,500 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonNettyByteBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonNettyByteBufferAllocator;
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.apache.qpid.protonj2.client.util.IOExceptionSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.FixedRecvByteBufAllocator;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+
+/**
+ * TCP based transport that uses Netty as the underlying IO layer.
+ */
+public class TcpTransport implements Transport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TcpTransport.class);
+
+    protected final AtomicBoolean connected = new AtomicBoolean();
+    protected final AtomicBoolean closed = new AtomicBoolean();
+    protected final CountDownLatch connectedLatch = new CountDownLatch(1);
+    protected final TransportOptions options;
+    protected final SslOptions sslOptions;
+    protected final Bootstrap bootstrap;
+
+    protected Channel channel;
+    protected volatile IOException failureCause;
+    protected String host;
+    protected int port;
+    protected TransportListener listener;
+
+    /**
+     * Create a new {@link TcpTransport} instance with the given configuration.
+     *
+     * @param bootstrap
+     *        the Netty {@link Bootstrap} that this transport's IO layer is bound to.
+     * @param options
+     *        the {@link TransportOptions} used to configure the socket connection.
+     * @param sslOptions
+     * 		  the {@link SslOptions} to use if the options indicate SSL is enabled.
+     */
+    public TcpTransport(Bootstrap bootstrap, TransportOptions options, SslOptions sslOptions) {
+        if (options == null) {
+            throw new IllegalArgumentException("Transport Options cannot be null");
+        }
+
+        if (sslOptions == null) {
+            throw new IllegalArgumentException("Transport SSL Options cannot be null");
+        }
+
+        if (bootstrap == null) {
+            throw new IllegalArgumentException("A transport must have an assigned Bootstrap before connect.");
+        }
+
+        this.sslOptions = sslOptions;
+        this.options = options;
+        this.bootstrap = bootstrap;
+    }
+
+    @Override
+    public TcpTransport connect(String host, int port, TransportListener listener) throws IOException {
+        if (closed.get()) {
+            throw new IllegalStateException("Transport has already been closed");
+        }
+
+        if (listener == null) {
+            throw new IllegalArgumentException("A transport listener must be set before connection attempts.");
+        }
+
+        if (host == null || host.isEmpty()) {
+            throw new IllegalArgumentException("Transport host value cannot be null");
+        }
+
+        if (port < 0 && options.defaultTcpPort() < 0 && (sslOptions.sslEnabled() && sslOptions.defaultSslPort() < 0)) {
+            throw new IllegalArgumentException("Transport port value must be a non-negative int value or a default port configured");
+        }
+
+        this.host = host;
+        this.listener = listener;
+
+        if (port > 0) {
+            this.port = port;
+        } else {
+            if (sslOptions.sslEnabled()) {
+                this.port = sslOptions.defaultSslPort();
+            } else {
+                this.port = options.defaultTcpPort();
+            }
+        }
+
+        bootstrap.handler(new ChannelInitializer<>() {
+            @Override
+            public void initChannel(Channel transportChannel) throws Exception {
+                channel = transportChannel;
+                configureChannel(transportChannel);
+                try {
+                    listener.transportInitialized(TcpTransport.this);
+                } catch (Throwable initError) {
+                    LOG.warn("Error during initialization of channel from Transport Listener");
+                    handleTransportFailure(transportChannel, IOExceptionSupport.create(initError));
+                    throw initError;
+                }
+            }
+        });
+
+        configureNetty(bootstrap, options);
+
+        bootstrap.connect(getHost(), getPort()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+
+        return this;
+    }
+
+    @Override
+    public void awaitConnect() throws InterruptedException, IOException {
+        connectedLatch.await();
+        if (!connected.get()) {
+            if (failureCause != null) {
+                throw failureCause;
+            } else {
+                throw new IOException("Transport was closed before a connection was established.");
+            }
+        }
+    }
+
+    @Override
+    public boolean isConnected() {
+        return connected.get();
+    }
+
+    @Override
+    public boolean isSecure() {
+        return sslOptions.sslEnabled();
+    }
+
+    @Override
+    public String getHost() {
+        return host;
+    }
+
+    @Override
+    public int getPort() {
+        return port;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (closed.compareAndSet(false, true)) {
+            connected.set(false);
+            connectedLatch.countDown();
+            if (channel != null) {
+                channel.close().syncUninterruptibly();
+            }
+        }
+    }
+
+    @Override
+    public ProtonBufferAllocator getBufferAllocator() {
+        return new ProtonNettyByteBufferAllocator() {
+
+            @Override
+            public ProtonBuffer outputBuffer(int initialCapacity) {
+                return new ProtonNettyByteBuffer(channel.alloc().ioBuffer(initialCapacity));
+            }
+
+            @Override
+            public ProtonBuffer outputBuffer(int initialCapacity, int maximumCapacity) {
+                return new ProtonNettyByteBuffer(channel.alloc().ioBuffer(initialCapacity, maximumCapacity));
+            }
+        };
+     }
+
+    @Override
+    public TcpTransport write(ProtonBuffer output) throws IOException {
+        return write(output, null);
+    }
+
+    @Override
+    public TcpTransport write(ProtonBuffer output, Runnable onComplete) throws IOException {
+        checkConnected(output);
+        LOG.trace("Attempted write of buffer: {}", output);
+        if (onComplete == null) {
+            channel.write(toOutputBuffer(output), channel.voidPromise());
+        } else {
+            channel.write(toOutputBuffer(output), channel.newPromise().addListener(new GenericFutureListener<Future<? super Void>>() {
+
+                @Override
+                public void operationComplete(Future<? super Void> future) throws Exception {
+                    if (future.isSuccess()) {
+                        onComplete.run();
+                    }
+                }
+            }));
+        }
+        return this;
+    }
+
+    @Override
+    public TcpTransport writeAndFlush(ProtonBuffer output) throws IOException {
+        return writeAndFlush(output, null);
+    }
+
+    @Override
+    public TcpTransport writeAndFlush(ProtonBuffer output, Runnable onComplete) throws IOException {
+        checkConnected(output);
+        LOG.trace("Attempted write and flush of buffer: {}", output);
+        if (onComplete == null) {
+            channel.writeAndFlush(toOutputBuffer(output), channel.voidPromise());
+        } else {
+            channel.writeAndFlush(toOutputBuffer(output), channel.newPromise().addListener(new GenericFutureListener<Future<? super Void>>() {
+
+                @Override
+                public void operationComplete(Future<? super Void> future) throws Exception {
+                    if (future.isSuccess()) {
+                        onComplete.run();
+                    }
+                }
+            }));
+        }
+        return this;
+    }
+
+    @Override
+    public TcpTransport flush() throws IOException {
+        checkConnected();
+        LOG.trace("Attempted flush of pending writes");
+        channel.flush();
+        return this;
+    }
+
+    @Override
+    public TransportListener getTransportListener() {
+        return listener;
+    }
+
+    @Override
+    public TransportOptions getTransportOptions() {
+        return options.clone();
+    }
+
+    @Override
+    public SslOptions getSslOptions() {
+        return sslOptions.clone();
+    }
+
+    @Override
+    public Principal getLocalPrincipal() {
+        Principal result = null;
+
+        if (isSecure()) {
+            SslHandler sslHandler = channel.pipeline().get(SslHandler.class);
+            result = sslHandler.engine().getSession().getLocalPrincipal();
+        }
+
+        return result;
+    }
+
+    protected final ByteBuf toOutputBuffer(final ProtonBuffer output) throws IOException {
+        final ByteBuf nettyBuf;
+
+        if (output instanceof ProtonNettyByteBuffer) {
+            nettyBuf = (ByteBuf) output.unwrap();
+        } else {
+            ProtonNettyByteBuffer wrapped = new ProtonNettyByteBuffer(channel.alloc().ioBuffer(output.getReadableBytes()));
+            wrapped.writeBytes(output);
+            nettyBuf = wrapped.unwrap();
+        }
+
+        return nettyBuf;
+    }
+
+    //----- Internal implementation details, can be overridden as needed -----//
+
+    protected void addAdditionalHandlers(ChannelPipeline pipeline) {
+
+    }
+
+    protected ChannelInboundHandlerAdapter createChannelHandler() {
+        return new NettyTcpTransportHandler();
+    }
+
+    //----- Event Handlers which can be overridden in subclasses -------------//
+
+    protected void handleConnected(Channel connectedChannel) throws Exception {
+        LOG.trace("Channel has become active! Channel is {}", connectedChannel);
+        channel = connectedChannel;
+        connected.set(true);
+        listener.transportConnected(this);
+        connectedLatch.countDown();
+    }
+
+    protected void handleTransportFailure(Channel failedChannel, Throwable cause) {
+        if (!closed.get()) {
+            LOG.trace("Transport indicates connection failure! Channel is {}", failedChannel);
+            failureCause = IOExceptionSupport.create(cause);
+            channel = failedChannel;
+            connected.set(false);
+            connectedLatch.countDown();
+
+            LOG.trace("Firing onTransportError listener");
+            if (channel.eventLoop().inEventLoop()) {
+                listener.transportError(failureCause);
+            } else {
+                channel.eventLoop().execute(() -> {
+                    listener.transportError(failureCause);
+                });
+            }
+        } else {
+            LOG.trace("Closed Transport signalled that the channel ended: {}", channel);
+        }
+    }
+
+    //----- State change handlers and checks ---------------------------------//
+
+    protected final void checkConnected() throws IOException {
+        if (!connected.get() || !channel.isActive()) {
+            throw new IOException("Cannot send to a non-connected transport.", failureCause);
+        }
+    }
+
+    private void checkConnected(ProtonBuffer output) throws IOException {
+        if (!connected.get() || !channel.isActive()) {
+            if (output instanceof ProtonNettyByteBuffer) {
+                ReferenceCountUtil.release(output.unwrap());
+            }
+            throw new IOException("Cannot send to a non-connected transport.", failureCause);
+        }
+    }
+
+    private void configureNetty(Bootstrap bootstrap, TransportOptions options) {
+        bootstrap.option(ChannelOption.TCP_NODELAY, options.tcpNoDelay());
+        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, options.connectTimeout());
+        bootstrap.option(ChannelOption.SO_KEEPALIVE, options.tcpKeepAlive());
+        bootstrap.option(ChannelOption.SO_LINGER, options.soLinger());
+
+        if (options.sendBufferSize() != -1) {
+            bootstrap.option(ChannelOption.SO_SNDBUF, options.sendBufferSize());
+        }
+
+        if (options.receiveBufferSize() != -1) {
+            bootstrap.option(ChannelOption.SO_RCVBUF, options.receiveBufferSize());
+            bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(options.receiveBufferSize()));
+        }
+
+        if (options.trafficClass() != -1) {
+            bootstrap.option(ChannelOption.IP_TOS, options.trafficClass());
+        }
+
+        if (options.localAddress() != null || options.localPort() != 0) {
+            if (options.localAddress() != null) {
+                bootstrap.localAddress(options.localAddress(), options.localPort());
+            } else {
+                bootstrap.localAddress(options.localPort());
+            }
+        }
+    }
+
+    private void configureChannel(final Channel channel) throws Exception {
+        if (isSecure()) {
+            final SslHandler sslHandler;
+            try {
+                sslHandler = SslSupport.createSslHandler(channel.alloc(), host, port, sslOptions);
+            } catch (Exception ex) {
+                LOG.warn("Error during initialization of channel from SSL Handler creation:");
+                handleTransportFailure(channel, IOExceptionSupport.create(ex));
+                throw IOExceptionSupport.create(ex);
+            }
+
+            channel.pipeline().addLast("ssl", sslHandler);
+        }
+
+        if (options.traceBytes()) {
+            channel.pipeline().addLast("logger", new LoggingHandler(getClass()));
+        }
+
+        addAdditionalHandlers(channel.pipeline());
+
+        channel.pipeline().addLast(createChannelHandler());
+    }
+
+    //----- Default implementation of Netty handler --------------------------//
+
+    protected abstract class NettyDefaultHandler<E> extends SimpleChannelInboundHandler<E> {
+
+        @Override
+        public final void channelRegistered(ChannelHandlerContext context) throws Exception {
+            channel = context.channel();
+        }
+
+        @Override
+        public void channelActive(ChannelHandlerContext context) throws Exception {
+            // In the Secure case we need to let the handshake complete before we
+            // trigger the connected event.
+            if (!isSecure()) {
+                handleConnected(context.channel());
+            } else {
+                SslHandler sslHandler = context.pipeline().get(SslHandler.class);
+                sslHandler.handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {
+                    @Override
+                    public void operationComplete(Future<Channel> future) throws Exception {
+                        if (future.isSuccess()) {
+                            LOG.trace("SSL Handshake has completed: {}", channel);
+                            handleConnected(channel);
+                        } else {
+                            LOG.trace("SSL Handshake has failed: {}", channel);
+                            handleTransportFailure(channel, future.cause());
+                        }
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext context) throws Exception {
+            handleTransportFailure(context.channel(), new IOException("Remote closed connection unexpectedly"));
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
+            handleTransportFailure(context.channel(), cause);
+        }
+    }
+
+    //----- Handle binary data over socket connections -----------------------//
+
+    protected class NettyTcpTransportHandler extends NettyDefaultHandler<ByteBuf> {
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
+            LOG.trace("New data read: {}", buffer);
+
+            final ProtonNettyByteBuffer wrapped = new ProtonNettyByteBuffer(buffer);
+
+            // Avoid all doubts to the contrary
+            if (channel.eventLoop().inEventLoop()) {
+                listener.transportRead(wrapped);
+            } else {
+                channel.eventLoop().execute(() -> {
+                    listener.transportRead(wrapped);
+                });
+            }
+        }
+    }
+
+    @Override
+    public URI getRemoteURI() {
+        if (host != null) {
+            try {
+                return new URI(getScheme(), null, host, port, null, null, null);
+            } catch (URISyntaxException e) {
+            }
+        }
+
+        return null;
+    }
+
+    protected String getScheme() {
+        return isSecure() ? "ssl" : "tcp";
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/Transport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/Transport.java
new file mode 100644
index 0000000..c732820
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/Transport.java
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferAllocator;
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+
+/**
+ * Base class for all QpidJMS Transport instances.
+ */
+public interface Transport {
+
+    /**
+     * Performs the connect operation for the implemented Transport type such as
+     * a TCP socket connection, SSL/TLS handshake etc.  The connection operation
+     * itself will be performed as an asynchronous operation with the success or
+     * failure being communicated to the event point
+     * {@link TransportListener#transportError(Throwable)}.  If the users wishes
+     * to perform a block on connect outcome the {@link #awaitConnect()} method
+     * will wait for and or throw an error based on the connect outcome.
+     *
+     * @param host
+     *      The remote host that this {@link Transport} should attempt to connect to.
+     * @param port
+     *      The port on the remote host that this {@link Transport} should attempt to bind to.
+     * @param listener
+     *      The {@link TransportListener} that will handle {@link Transport} events.
+     *
+     * @return this {@link Transport} instance.
+     *
+     * @throws IOException if an error occurs while attempting the connect.
+     */
+    Transport connect(String host, int port, TransportListener listener) throws IOException;
+
+    /**
+     * Waits interruptibly for the {@link Transport} to connect to the remote that was
+     * indicated in the {@link #connect(String, int, TransportListener)} call.
+     *
+     * @throws InterruptedException
+     *      If the wait mechanism was interrupted while waiting for a successful connect.
+     * @throws IOException
+     *      If the {@link Transport} failed to connect or was closed before connected.
+     */
+    void awaitConnect() throws InterruptedException, IOException;
+
+    /**
+     * @return true if transport is connected or false if the connection is down.
+     */
+    boolean isConnected();
+
+    /**
+     * @return true if transport is connected using a secured channel (SSL).
+     */
+    boolean isSecure();
+
+    /**
+     * Close the Transport, no additional send operations are accepted.
+     *
+     * @throws IOException if an error occurs while closing the connection.
+     */
+    void close() throws IOException;
+
+    /**
+     * Gets a buffer allocator that can produce {@link ProtonBuffer} instance that may be
+     * optimized for use with the underlying transport implementation.
+     *
+     * @return a {@link ProtonBufferAllocator} that creates transport friendly buffers.
+     */
+    ProtonBufferAllocator getBufferAllocator();
+
+    /**
+     * Writes a chunk of data over the Transport connection without performing an
+     * explicit flush on the transport.
+     *
+     * @param output
+     *        The buffer of data that is to be transmitted.
+     *
+     * @return this {@link Transport} instance.
+     *
+     * @throws IOException if an error occurs during the write operation.
+     */
+    Transport write(ProtonBuffer output) throws IOException;
+
+    /**
+     * Writes a chunk of data over the Transport connection without performing an
+     * explicit flush on the transport.  This method allows for a completion callback
+     * that is signaled when the actual low level IO operation is completed which could
+     * be after this method has returned.
+     *
+     * @param output
+     *        The buffer of data that is to be transmitted.
+     * @param ioComplete
+     *        A {@link Runnable} that is invoked when the IO operation completes successfully.
+     *
+     * @return this {@link Transport} instance.
+     *
+     * @throws IOException if an error occurs during the write operation.
+     */
+    Transport write(ProtonBuffer output, Runnable ioComplete) throws IOException;
+
+    /**
+     * Writes a chunk of data over the Transport connection and requests a flush of
+     * all pending queued write operations
+     *
+     * @param output
+     *        The buffer of data that is to be transmitted.
+     *
+     * @return this {@link Transport} instance.
+     *
+     * @throws IOException if an error occurs during the write operation.
+     */
+    Transport writeAndFlush(ProtonBuffer output) throws IOException;
+
+    /**
+     * Writes a chunk of data over the Transport connection and requests a flush of
+     * all pending queued write operations
+     *
+     * @param output
+     *        The buffer of data that is to be transmitted.
+     * @param ioComplete
+     *        A {@link Runnable} that is invoked when the IO operation completes successfully.
+     *
+     * @return this {@link Transport} instance.
+     *
+     * @throws IOException if an error occurs during the write operation.
+     */
+    Transport writeAndFlush(ProtonBuffer output, Runnable ioComplete) throws IOException;
+
+    /**
+     * Request a flush of all pending writes to the underlying connection.
+     *
+     * @return this {@link Transport} instance.
+     *
+     * @throws IOException if an error occurs during the flush operation.
+     */
+    Transport flush() throws IOException;
+
+    /**
+     * Gets the currently set TransportListener instance
+     *
+     * @return the current TransportListener or null if none set.
+     */
+    TransportListener getTransportListener();
+
+    /**
+     * @return a {@link TransportOptions} instance copied from the immutable options given at create time..
+     */
+    TransportOptions getTransportOptions();
+
+    /**
+     * @return a {@link SslOptions} instance copied from the immutable options given at create time..
+     */
+    SslOptions getSslOptions();
+
+    /**
+     * @return the host name or IP address that the transport connects to.
+     */
+    String getHost();
+
+    /**
+     * @return the port that the transport connects to.
+     */
+    int getPort();
+
+    /**
+     * Returns a URI that contains some meaningful information about the remote connection such as a
+     * scheme that reflects the transport type and the remote host and port that the connection was
+     * instructed to connect to.  If called before the {@link #connect(String, int, TransportListener)}
+     * method this method returns <code>null</code>.
+     *
+     * @return a URI that reflects a meaningful view of the {@link Transport} remote connection details.
+     */
+    URI getRemoteURI();
+
+    /**
+     * @return the local principal for a Transport that is using a secure connection.
+     */
+    Principal getLocalPrincipal();
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/TransportListener.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/TransportListener.java
new file mode 100644
index 0000000..a1ff690
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/TransportListener.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Listener interface that should be implemented by users of the various
+ * QpidJMS Transport classes.
+ */
+public interface TransportListener {
+
+    /**
+     * Called immediately before the transport attempts to connect to the remote peer
+     * but following all {@link Transport} initialization.  The Transport configuration
+     * is now static and the event handler can update any internal state or configure
+     * additional resources based on the configured and prepared {@link Transport}.
+     *
+     * @param transport
+     *      The transport that is now fully connected and ready to perform IO operations.
+     */
+    void transportInitialized(Transport transport);
+
+    /**
+     * Called after the transport has successfully connected to the remote and performed any
+     * required handshakes such as SSL or Web Sockets handshaking and the connection is now
+     * considered open.
+     *
+     * @param transport
+     *      The transport that is now fully connected and ready to perform IO operations.
+     */
+    void transportConnected(Transport transport);
+
+    /**
+     * Called when new incoming data has become available for processing by the {@link Transport}
+     * user.
+     *
+     * @param incoming
+     *        the next incoming packet of data.
+     */
+    void transportRead(ProtonBuffer incoming);
+
+    /**
+     * Called when an error occurs during normal Transport operations such as SSL handshake
+     * or remote connection dropped.  Once this error callback is triggered the {@link Transport}
+     * is considered to be failed and should be closed.
+     *
+     * @param cause
+     *        the error that triggered this event.
+     */
+    void transportError(Throwable cause);
+
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/WebSocketTransport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/WebSocketTransport.java
new file mode 100644
index 0000000..8a1f8e0
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/WebSocketTransport.java
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonNettyByteBuffer;
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketVersion;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.concurrent.ScheduledFuture;
+
+/**
+ * Netty based WebSockets Transport that wraps and extends the TCP Transport.
+ */
+public class WebSocketTransport extends TcpTransport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(WebSocketTransport.class);
+
+    private static final String AMQP_SUB_PROTOCOL = "amqp";
+
+    private ScheduledFuture<?> handshakeTimeoutFuture;
+
+    /**
+     * Create a new {@link WebSocketTransport} instance with the given configuration.
+     *
+     * @param bootstrap
+     *        the {@link Bootstrap} that this transport's IO is bound to.
+     * @param options
+     *        the {@link TransportOptions} used to configure the socket connection.
+     * @param sslOptions
+     *        the {@link SslOptions} to use if the options indicate SSL is enabled.
+     */
+    public WebSocketTransport(Bootstrap bootstrap, TransportOptions options, SslOptions sslOptions) {
+        super(bootstrap, options, sslOptions);
+    }
+
+    @Override
+    public WebSocketTransport write(ProtonBuffer output, Runnable onComplete) throws IOException {
+        checkConnected();
+        int length = output.getReadableBytes();
+        if (length == 0) {
+            return this;
+        }
+
+        LOG.trace("Attempted write of: {} bytes", length);
+
+        if (onComplete == null) {
+            channel.write(new BinaryWebSocketFrame(toOutputBuffer(output)), channel.voidPromise());
+        } else {
+            channel.write(new BinaryWebSocketFrame(toOutputBuffer(output)), channel.newPromise().addListener(new GenericFutureListener<Future<? super Void>>() {
+
+                @Override
+                public void operationComplete(Future<? super Void> future) throws Exception {
+                    if (future.isSuccess()) {
+                        onComplete.run();
+                    }
+                }
+            }));
+        }
+
+        return this;
+    }
+
+    @Override
+    public WebSocketTransport writeAndFlush(ProtonBuffer output, Runnable onComplete) throws IOException {
+        checkConnected();
+        int length = output.getReadableBytes();
+        if (length == 0) {
+            return this;
+        }
+
+        LOG.trace("Attempted write and flush of: {} bytes", length);
+
+        if (onComplete == null) {
+            channel.writeAndFlush(new BinaryWebSocketFrame(toOutputBuffer(output)), channel.voidPromise());
+        } else {
+            channel.writeAndFlush(new BinaryWebSocketFrame(toOutputBuffer(output)), channel.newPromise().addListener(new GenericFutureListener<Future<? super Void>>() {
+
+                @Override
+                public void operationComplete(Future<? super Void> future) throws Exception {
+                    if (future.isSuccess()) {
+                        onComplete.run();
+                    }
+                }
+            }));
+        }
+
+        return this;
+    }
+
+    @Override
+    public URI getRemoteURI() {
+        if (host != null) {
+            try {
+                return new URI(getScheme(), null, host, port, options.webSocketPath(), null, null);
+            } catch (URISyntaxException e) {
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    protected ChannelInboundHandlerAdapter createChannelHandler() {
+        return new NettyWebSocketTransportHandler();
+    }
+
+    @Override
+    protected void addAdditionalHandlers(ChannelPipeline pipeline) {
+        pipeline.addLast(new HttpClientCodec());
+        pipeline.addLast(new HttpObjectAggregator(8192));
+    }
+
+    @Override
+    protected void handleConnected(Channel channel) throws Exception {
+        LOG.trace("Channel has become active, awaiting WebSocket handshake! Channel is {}", channel);
+    }
+
+    @Override
+    protected String getScheme() {
+        return isSecure() ? "wss" : "ws";
+    }
+
+    //----- Handle connection events -----------------------------------------//
+
+    private class NettyWebSocketTransportHandler extends NettyDefaultHandler<Object> {
+
+        private final WebSocketClientHandshaker handshaker;
+
+        public NettyWebSocketTransportHandler() {
+            DefaultHttpHeaders headers = new DefaultHttpHeaders();
+
+            options.webSocketHeaders().forEach((key, value) -> {
+                headers.set(key, value);
+            });
+
+            handshaker = WebSocketClientHandshakerFactory.newHandshaker(
+                getRemoteURI(), WebSocketVersion.V13, AMQP_SUB_PROTOCOL,
+                true, headers, options.webSocketMaxFrameSize());
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext context) throws Exception {
+            if (handshakeTimeoutFuture != null) {
+                handshakeTimeoutFuture.cancel(false);
+            }
+
+            super.channelInactive(context);
+        }
+
+        @Override
+        public void channelActive(ChannelHandlerContext context) throws Exception {
+            handshaker.handshake(context.channel());
+
+            handshakeTimeoutFuture = context.executor().schedule(()-> {
+                LOG.trace("WebSocket handshake timed out! Channel is {}", context.channel());
+                if (!handshaker.isHandshakeComplete()) {
+                    WebSocketTransport.super.handleTransportFailure(channel, new IOException("WebSocket handshake timed out"));
+                }
+            }, getTransportOptions().connectTimeout(), TimeUnit.MILLISECONDS);
+
+            super.channelActive(context);
+        }
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, Object message) throws Exception {
+            LOG.trace("New data read: incoming: {}", message);
+
+            Channel ch = ctx.channel();
+            if (!handshaker.isHandshakeComplete()) {
+                handshaker.finishHandshake(ch, (FullHttpResponse) message);
+                LOG.trace("WebSocket Client connected! {}", ctx.channel());
+                // Now trigger super processing as we are really connected.
+                if (handshakeTimeoutFuture.cancel(false)) {
+                    WebSocketTransport.super.handleConnected(ch);
+                }
+
+                return;
+            }
+
+            // We shouldn't get this since we handle the handshake previously.
+            if (message instanceof FullHttpResponse) {
+                FullHttpResponse response = (FullHttpResponse) message;
+                throw new IllegalStateException(
+                    "Unexpected FullHttpResponse (getStatus=" + response.status() +
+                    ", content=" + response.content().toString(StandardCharsets.UTF_8) + ')');
+            }
+
+            WebSocketFrame frame = (WebSocketFrame) message;
+            if (frame instanceof TextWebSocketFrame) {
+                TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
+                LOG.warn("WebSocket Client received message: " + textFrame.text());
+                ctx.fireExceptionCaught(new IOException("Received invalid frame over WebSocket."));
+            } else if (frame instanceof BinaryWebSocketFrame) {
+                BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame;
+                LOG.trace("WebSocket Client received data: {} bytes", binaryFrame.content().readableBytes());
+                listener.transportRead(new ProtonNettyByteBuffer(binaryFrame.content()));
+            } else if (frame instanceof ContinuationWebSocketFrame) {
+                ContinuationWebSocketFrame continuationFrame = (ContinuationWebSocketFrame) frame;
+                LOG.trace("WebSocket Client received data continuation: {} bytes", continuationFrame.content().readableBytes());
+                listener.transportRead(new ProtonNettyByteBuffer(continuationFrame.content()));
+            } else if (frame instanceof PingWebSocketFrame) {
+                LOG.trace("WebSocket Client received ping, response with pong");
+                ch.write(new PongWebSocketFrame(frame.content()));
+            } else if (frame instanceof CloseWebSocketFrame) {
+                LOG.trace("WebSocket Client received closing");
+                ch.close();
+            }
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/X509AliasKeyManager.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/X509AliasKeyManager.java
new file mode 100644
index 0000000..6fad74b
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/X509AliasKeyManager.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+/**
+ * An X509ExtendedKeyManager wrapper which always chooses and only
+ * returns the given alias, and defers retrieval to the delegate
+ * key manager.
+ */
+public class X509AliasKeyManager extends X509ExtendedKeyManager {
+    private X509ExtendedKeyManager delegate;
+    private String alias;
+
+    public X509AliasKeyManager(String alias, X509ExtendedKeyManager delegate) throws IllegalArgumentException {
+        if (alias == null) {
+            throw new IllegalArgumentException("The given key alias must not be null.");
+        }
+
+        this.alias = alias;
+        this.delegate = delegate;
+    }
+
+    @Override
+    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+        return alias;
+    }
+
+    @Override
+    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+        return alias;
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain(String alias) {
+        return delegate.getCertificateChain(alias);
+    }
+
+    @Override
+    public String[] getClientAliases(String keyType, Principal[] issuers) {
+        return new String[] { alias };
+    }
+
+    @Override
+    public PrivateKey getPrivateKey(String alias) {
+        return delegate.getPrivateKey(alias);
+    }
+
+    @Override
+    public String[] getServerAliases(String keyType, Principal[] issuers) {
+        return new String[] { alias };
+    }
+
+    @Override
+    public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+        return alias;
+    }
+
+    @Override
+    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+        return alias;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/DeliveryQueue.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/DeliveryQueue.java
new file mode 100644
index 0000000..70dcb9d
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/DeliveryQueue.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.impl.ClientDelivery;
+
+/**
+ * Queue based storage interface for inbound AMQP {@link Delivery} objects.
+ */
+public interface DeliveryQueue {
+
+    /**
+     * Adds the given {@link Delivery} to the end of the Delivery queue.
+     *
+     * @param delivery
+     *        The in-bound Delivery to enqueue.
+     */
+    void enqueue(ClientDelivery delivery);
+
+    /**
+     * Adds the given {@link Delivery} to the front of the queue.
+     *
+     * @param delivery
+     *        The in-bound Delivery to enqueue.
+     */
+    void enqueueFirst(ClientDelivery delivery);
+
+    /**
+     * Used to get an {@link Delivery}. The amount of time this method blocks is based on the timeout value
+     * that is supplied to it.
+     *
+     * <ul>
+     *  <li>
+     *   If the timeout value is less than zero the dequeue operation blocks until a Delivery
+     *   is enqueued or the queue is stopped.
+     *  </li>
+     *  <li>
+     *   If the timeout value is zero the dequeue operation will not block and will either return
+     *   the next Delivery on the Queue or null to indicate the queue is empty.
+     *  </li>
+     *  <li>
+     *   If the timeout value is greater than zero then the method will either return the next Delivery
+     *   in the queue or block until the timeout (in milliseconds) has expired or until a new Delivery
+     *   is placed onto the queue.
+     *  </li>
+     * </ul>
+     *
+     * @param timeout
+     *      The amount of time to wait for an entry to be added before returning null.
+     *
+     * @return null if we timeout or if the {@link Receiver} is closed.
+     *
+     * @throws InterruptedException if the wait is interrupted.
+     */
+    ClientDelivery dequeue(long timeout) throws InterruptedException;
+
+    /**
+     * Used to get an enqueued {@link Delivery} if on exists, otherwise returns null.
+     *
+     * @return the next Delivery in the Queue if one exists, otherwise null.
+     */
+    ClientDelivery dequeueNoWait();
+
+    /**
+     * Starts the Delivery Queue.  An non-started Queue will always return null for
+     * any of the Queue methods.
+     */
+    void start();
+
+    /**
+     * Stops the Delivery Queue.  Deliveries cannot be read from the Queue when it is in
+     * the stopped state and any waiters will be woken.
+     */
+    void stop();
+
+    /**
+     * Closes the Delivery Queue.  No Delivery can be added or removed from the Queue
+     * once it has entered the closed state.
+     */
+    void close();
+
+    /**
+     * @return true if the Queue is not in the stopped or closed state.
+     */
+    boolean isRunning();
+
+    /**
+     * @return true if the Queue has been closed.
+     */
+    boolean isClosed();
+
+    /**
+     * @return true if there are no deliveries in the queue.
+     */
+    boolean isEmpty();
+
+    /**
+     * Returns the number of deliveries currently in the Queue.  This value is only
+     * meaningful at the time of the call as the size of the Queue changes rapidly
+     * as deliveries arrive and are consumed.
+     *
+     * @return the current number of {@link Delivery} objects in the Queue.
+     */
+    int size();
+
+    /**
+     * Clears the Queue of any queued {@link Delivery} values.
+     */
+    void clear();
+
+}
\ No newline at end of file
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/FifoDeliveryQueue.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/FifoDeliveryQueue.java
new file mode 100644
index 0000000..947dfe8
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/FifoDeliveryQueue.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.impl.ClientDelivery;
+
+/**
+ * Simple first in / first out {@link Delivery} Queue.
+ */
+public final class FifoDeliveryQueue implements DeliveryQueue {
+
+    protected static final AtomicIntegerFieldUpdater<FifoDeliveryQueue> STATE_FIELD_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(FifoDeliveryQueue.class, "state");
+
+    protected static final int CLOSED = 0;
+    protected static final int STOPPED = 1;
+    protected static final int RUNNING = 2;
+
+    private volatile int state = STOPPED;
+
+    protected final ReentrantLock lock = new ReentrantLock();
+    protected final Condition condition = lock.newCondition();
+
+    protected final Deque<ClientDelivery> queue;
+
+    public FifoDeliveryQueue(int queueDepth) {
+        this.queue = new ArrayDeque<ClientDelivery>(Math.max(1, queueDepth));
+    }
+
+    @Override
+    public void enqueueFirst(ClientDelivery envelope) {
+        lock.lock();
+        try {
+            queue.addFirst(envelope);
+            condition.signal();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public void enqueue(ClientDelivery envelope) {
+        lock.lock();
+        try {
+            queue.addLast(envelope);
+            condition.signal();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+
+    @Override
+    public ClientDelivery dequeue(long timeout) throws InterruptedException {
+        lock.lock();
+        try {
+            // Wait until the receiver is ready to deliver messages.
+            while (timeout != 0 && isRunning() && queue.isEmpty()) {
+                if (timeout == -1) {
+                    condition.await();
+                } else {
+                    long start = System.currentTimeMillis();
+                    condition.await(timeout, TimeUnit.MILLISECONDS);
+                    timeout = Math.max(timeout + start - System.currentTimeMillis(), 0);
+                }
+            }
+
+            if (!isRunning()) {
+                return null;
+            }
+
+            return queue.pollFirst();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public ClientDelivery dequeueNoWait() {
+        lock.lock();
+        try {
+            if (!isRunning()) {
+                return null;
+            }
+
+            return queue.pollFirst();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public void start() {
+        if (STATE_FIELD_UPDATER.compareAndSet(this, STOPPED, RUNNING)) {
+            lock.lock();
+            try {
+                condition.signalAll();
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    @Override
+    public void stop() {
+        if (STATE_FIELD_UPDATER.compareAndSet(this, RUNNING, STOPPED)) {
+            lock.lock();
+            try {
+                condition.signalAll();
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        if (STATE_FIELD_UPDATER.getAndSet(this, CLOSED) > CLOSED) {
+            lock.lock();
+            try {
+                condition.signalAll();
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    @Override
+    public boolean isRunning() {
+        return state == RUNNING;
+    }
+
+    @Override
+    public boolean isClosed() {
+        return state == CLOSED;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        lock.lock();
+        try {
+            return queue.isEmpty();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public int size() {
+        lock.lock();
+        try {
+            return queue.size();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public void clear() {
+        lock.lock();
+        try {
+            queue.clear();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public String toString() {
+        lock.lock();
+        try {
+            return queue.toString();
+        } finally {
+            lock.unlock();
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/IOExceptionSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/IOExceptionSupport.java
new file mode 100644
index 0000000..bf2f08e
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/IOExceptionSupport.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.io.IOException;
+
+/**
+ * Used to make throwing IOException instances easier.
+ */
+public abstract class IOExceptionSupport {
+
+    /**
+     * Checks the given cause to determine if it's already an IOException type and
+     * if not creates a new IOException to wrap it.
+     *
+     * @param cause
+     *        The initiating exception that should be cast or wrapped.
+     *
+     * @return an IOException instance.
+     */
+    public static IOException create(Throwable cause) {
+        if (cause instanceof IOException) {
+            return (IOException) cause;
+        }
+
+        String message = cause.getMessage();
+        if (message == null || message.length() == 0) {
+            message = cause.toString();
+        }
+
+        return new IOException(message, cause);
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/IdGenerator.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/IdGenerator.java
new file mode 100644
index 0000000..539f9d1
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/IdGenerator.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Generator for Globally unique Strings used to identify resources within a given Connection.
+ */
+public class IdGenerator {
+
+    private final String prefix;
+    private final AtomicLong sequence = new AtomicLong(1);
+
+    public static final String DEFAULT_PREFIX = "ID:";
+
+    /**
+     * Construct an IdGenerator using the given prefix value as the initial
+     * prefix entry for all Ids generated (default is 'ID:').
+     *
+     * @param prefix
+     *      The prefix value that is applied to all generated IDs.
+     */
+    public IdGenerator(String prefix) {
+        this.prefix = prefix;
+    }
+
+    /**
+     * Construct an IdGenerator using the default prefix value.
+     */
+    public IdGenerator() {
+        this(DEFAULT_PREFIX);
+    }
+
+    /**
+     * Generate a unique id using the configured characteristics.
+     *
+     * @return a newly generated unique id value.
+     */
+    public String generateId() {
+        StringBuilder sb = new StringBuilder(64);
+
+        sb.append(prefix);
+        sb.append(UUID.randomUUID());
+        sb.append(":");
+        sb.append(sequence.getAndIncrement());
+
+        return sb.toString();
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/ReconnectionURIPool.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/ReconnectionURIPool.java
new file mode 100644
index 0000000..ece6500
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/ReconnectionURIPool.java
@@ -0,0 +1,268 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the list of available reconnect URIs that are used to connect
+ * and recover a connection.
+ */
+public class ReconnectionURIPool {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ReconnectionURIPool.class);
+
+    private final LinkedList<URI> uris;
+
+    public ReconnectionURIPool() {
+        this.uris = new LinkedList<URI>();
+    }
+
+    public ReconnectionURIPool(List<URI> backups) {
+        this.uris = new LinkedList<URI>();
+
+        if (backups != null) {
+            for (URI uri : backups) {
+                this.add(uri);
+            }
+        }
+    }
+
+    /**
+     * @return the current size of the URI pool.
+     */
+    public int size() {
+        synchronized (uris) {
+            return uris.size();
+        }
+    }
+
+    /**
+     * @return true if the URI pool is empty.
+     */
+    public boolean isEmpty() {
+        synchronized (uris) {
+            return uris.isEmpty();
+        }
+    }
+
+    /**
+     * Returns the next URI in the pool of URIs.  The URI will be shifted to the
+     * end of the list and not be attempted again until the full list has been
+     * returned once.
+     *
+     * @return the next URI that should be used for a connection attempt.
+     */
+    public URI getNext() {
+        URI next = null;
+        synchronized (uris) {
+            if (!uris.isEmpty()) {
+                next = uris.removeFirst();
+                uris.addLast(next);
+            }
+        }
+
+        return next;
+    }
+
+    /**
+     * Randomizes the order of the list of URIs contained within the pool.
+     */
+    public void shuffle() {
+        synchronized (uris) {
+            Collections.shuffle(uris);
+        }
+    }
+
+    /**
+     * Adds a new URI to the pool if not already contained within.
+     *
+     * @param uri
+     *        The new URI to add to the pool.
+     */
+    public void add(URI uri) {
+        if (uri == null) {
+            return;
+        }
+
+        synchronized (uris) {
+            if (!contains(uri)) {
+                uris.add(uri);
+            }
+        }
+    }
+
+    /**
+     * Adds a list of new URIs to the pool if not already contained within.
+     *
+     * @param additions
+     *        The new list of URIs to add to the pool.
+     */
+    public void addAll(List<URI> additions) {
+        if (additions == null || additions.isEmpty()) {
+            return;
+        }
+
+        synchronized (uris) {
+            for (URI uri : additions) {
+                add(uri);
+            }
+        }
+    }
+
+    /**
+     * Adds a new URI to the pool if not already contained within.
+     *
+     * The URI is added to the head of the pooled URIs and will be the next value that
+     * is returned from the pool.
+     *
+     * @param uri
+     *        The new URI to add to the pool.
+     */
+    public void addFirst(URI uri) {
+        if (uri == null) {
+            return;
+        }
+
+        synchronized (uris) {
+            if (!contains(uri)) {
+                uris.addFirst(uri);
+            }
+        }
+    }
+
+    /**
+     * Remove a URI from the pool if present, otherwise has no effect.
+     *
+     * @param uri
+     *        The URI to attempt to remove from the pool.
+     *
+     * @return true if the given URI was removed from the pool.
+     */
+    public boolean remove(URI uri) {
+        if (uri == null) {
+            return false;
+        }
+
+        synchronized (uris) {
+            for (URI candidate : uris) {
+                if (compareURIs(uri, candidate)) {
+                    return uris.remove(candidate);
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Removes all currently configured URIs from the pool, no new URIs will be
+     * served from this pool until new ones are added.
+     */
+    public void removeAll() {
+        synchronized (uris) {
+            uris.clear();
+        }
+    }
+
+    /**
+     * Removes all currently configured URIs from the pool and replaces them with
+     * the new set given.
+     *
+     * @param replacements
+     * 		The new set of reconnect URIs to serve from this pool.
+     */
+    public void replaceAll(List<URI> replacements) {
+        synchronized (uris) {
+            uris.clear();
+            addAll(replacements);
+        }
+    }
+
+    /**
+     * Gets the current list of URIs. The returned list is a copy.
+     *
+     * @return a copy of the current list of URIs in the pool.
+     */
+    public List<URI> getList() {
+        synchronized (uris) {
+            return new ArrayList<>(uris);
+        }
+    }
+
+    @Override
+    public String toString() {
+        synchronized (uris) {
+            return "URI Pool { " + uris + " }";
+        }
+    }
+
+    //----- Internal methods that require the locks be held ------------------//
+
+    private boolean contains(URI newURI) {
+        boolean result = false;
+        for (URI uri : uris) {
+            if (compareURIs(newURI, uri)) {
+                result = true;
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    private boolean compareURIs(final URI first, final URI second) {
+        boolean result = false;
+        if (first == null || second == null) {
+            return result;
+        }
+
+        if (first.getPort() == second.getPort()) {
+            InetAddress firstAddr = null;
+            InetAddress secondAddr = null;
+            try {
+                firstAddr = InetAddress.getByName(first.getHost());
+                secondAddr = InetAddress.getByName(second.getHost());
+
+                if (firstAddr.equals(secondAddr)) {
+                    result = true;
+                }
+            } catch (IOException e) {
+                if (firstAddr == null) {
+                    LOG.error("Failed to Lookup INetAddress for URI[ " + first + " ] : " + e);
+                } else {
+                    LOG.error("Failed to Lookup INetAddress for URI[ " + second + " ] : " + e);
+                }
+
+                if (first.getHost().equalsIgnoreCase(second.getHost())) {
+                    result = true;
+                }
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/StopWatch.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/StopWatch.java
new file mode 100644
index 0000000..db650ab
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/StopWatch.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+/**
+ * A very simple stop watch.
+ * <p>
+ * This implementation is not thread safe and can only time one task at any given time.
+ */
+public final class StopWatch {
+
+    private long start;
+    private long stop;
+
+    /**
+     * Starts the stop watch
+     */
+    public StopWatch() {
+        this(true);
+    }
+
+    /**
+     * @return check if the {@link StopWatch} has already been started.
+     */
+    public boolean isStarted() {
+        return start != 0;
+    }
+
+    /**
+     * Creates the stop watch
+     *
+     * @param started whether it should start immediately
+     */
+    public StopWatch(boolean started) {
+        if (started) {
+            restart();
+        }
+    }
+
+    /**
+     * Starts or restarts the stop watch
+     */
+    public void restart() {
+        start = System.currentTimeMillis();
+        stop = 0;
+    }
+
+    /**
+     * Stops the stop watch
+     *
+     * @return the time taken in millis.
+     */
+    public long stop() {
+        stop = System.currentTimeMillis();
+        return taken();
+    }
+
+    /**
+     * Returns the time taken in millis.
+     *
+     * @return time in millis
+     */
+    public long taken() {
+        if (start > 0 && stop > 0) {
+            return stop - start;
+        } else if (start > 0) {
+            return System.currentTimeMillis() - start;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/StringArrayConverter.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/StringArrayConverter.java
new file mode 100644
index 0000000..796d159
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/StringArrayConverter.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Class for converting to/from String[] to be used instead of a
+ * {@link java.beans.PropertyEditor} which otherwise causes memory leaks as the
+ * JDK {@link java.beans.PropertyEditorManager} is a static class and has strong
+ * references to classes, causing problems in hot-deployment environments.
+ */
+public class StringArrayConverter {
+
+    public static String[] convertToStringArray(Object value) {
+        if (value == null) {
+            return null;
+        }
+
+        String text = value.toString();
+        if (text == null || text.isEmpty()) {
+            return null;
+        }
+
+        StringTokenizer stok = new StringTokenizer(text, ",");
+        final List<String> list = new ArrayList<String>();
+
+        while (stok.hasMoreTokens()) {
+            list.add(stok.nextToken());
+        }
+
+        String[] array = list.toArray(new String[list.size()]);
+        return array;
+    }
+
+    public static String convertToString(String[] value) {
+        if (value == null || value.length == 0) {
+            return null;
+        }
+
+        StringBuffer result = new StringBuffer(String.valueOf(value[0]));
+        for (int i = 1; i < value.length; i++) {
+            result.append(",").append(value[i]);
+        }
+
+        return result.toString();
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/ThreadPoolUtils.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/ThreadPoolUtils.java
new file mode 100644
index 0000000..c32657d
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/ThreadPoolUtils.java
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for working with thread pools {@link ExecutorService}.
+ */
+public final class ThreadPoolUtils {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ThreadPoolUtils.class);
+
+    public static final long DEFAULT_SHUTDOWN_AWAIT_TERMINATION = 10 * 1000L;
+
+    /**
+     * Shutdown the given executor service only (ie not graceful shutdown).
+     *
+     * @param executorService
+     *        The ExecutorService that is being shutdown.
+     *
+     * @see java.util.concurrent.ExecutorService#shutdown()
+     */
+    public static void shutdown(ExecutorService executorService) {
+        doShutdown(executorService, 0);
+    }
+
+    /**
+     * Shutdown now the given executor service aggressively.
+     *
+     * @param executorService
+     *        the executor service to shutdown now
+     *
+     * @return list of tasks that never commenced execution
+     *
+     * @see java.util.concurrent.ExecutorService#shutdownNow()
+     */
+    public static List<Runnable> shutdownNow(ExecutorService executorService) {
+        if (executorService == null) {
+            return Collections.emptyList();
+        }
+
+        List<Runnable> answer = null;
+        if (!executorService.isShutdown()) {
+            LOG.debug("Forcing shutdown of ExecutorService: {}", executorService);
+            answer = executorService.shutdownNow();
+            LOG.trace("Shutdown of ExecutorService: {} is shutdown: {} and terminated: {}.",
+                new Object[] { executorService, executorService.isShutdown(), executorService.isTerminated() });
+        }
+
+        if (answer == null) {
+            answer = Collections.emptyList();
+        }
+
+        return answer;
+    }
+
+    /**
+     * Shutdown the given executor service graceful at first, and then aggressively if the await
+     * termination timeout was hit.
+     * <p>
+     * This implementation invokes the
+     * {@link #shutdownGraceful(java.util.concurrent.ExecutorService, long)} with a timeout
+     * value of {@link #DEFAULT_SHUTDOWN_AWAIT_TERMINATION} millis.
+     *
+     * @param executorService
+     *        The ExecutorService that is being shutdown.
+     */
+    public static void shutdownGraceful(ExecutorService executorService) {
+        doShutdown(executorService, DEFAULT_SHUTDOWN_AWAIT_TERMINATION);
+    }
+
+    /**
+     * Shutdown the given executor service graceful at first, and then aggressively if the await
+     * termination timeout was hit.
+     * <p>
+     * Will try to perform an orderly shutdown by giving the running threads time to complete
+     * tasks, before going more aggressively by doing a
+     * {@link #shutdownNow(java.util.concurrent.ExecutorService)} which forces a shutdown. The
+     * parameter <code>shutdownAwaitTermination</code> is used as timeout value waiting for orderly
+     * shutdown to complete normally, before going aggressively.
+     *
+     * @param executorService
+     *        the executor service to shutdown
+     * @param shutdownAwaitTermination
+     *        timeout in millis to wait for orderly shutdown
+     */
+    public static void shutdownGraceful(ExecutorService executorService, long shutdownAwaitTermination) {
+        doShutdown(executorService, shutdownAwaitTermination);
+    }
+
+    private static void doShutdown(ExecutorService executorService, long shutdownAwaitTermination) {
+        if (executorService == null) {
+            return;
+        }
+
+        // shutting down a thread pool is a 2 step process. First we try graceful, and if
+        // that fails, then we go more aggressively and try shutting down again. In both
+        // cases we wait at most the given shutdown timeout value given
+        //
+        // total wait could then be 2 x shutdownAwaitTermination, but when we shutdown the
+        // 2nd time we are aggressive and thus we ought to shutdown much faster
+        if (!executorService.isShutdown()) {
+            boolean warned = false;
+            StopWatch watch = new StopWatch();
+
+            LOG.trace("Shutdown of ExecutorService: {} with await termination: {} millis", executorService, shutdownAwaitTermination);
+            executorService.shutdown();
+
+            if (shutdownAwaitTermination > 0) {
+                try {
+                    if (!awaitTermination(executorService, shutdownAwaitTermination)) {
+                        warned = true;
+                        LOG.warn("Forcing shutdown of ExecutorService: {} due first await termination elapsed.", executorService);
+                        executorService.shutdownNow();
+                        // we are now shutting down aggressively, so wait to see
+                        // if we can completely shutdown or not
+                        if (!awaitTermination(executorService, shutdownAwaitTermination)) {
+                            LOG.warn("Cannot completely force shutdown of ExecutorService: {} due second await termination elapsed.", executorService);
+                        }
+                    }
+                } catch (InterruptedException e) {
+                    warned = true;
+                    LOG.warn("Forcing shutdown of ExecutorService: {} due interrupted.", executorService);
+                    // we were interrupted during shutdown, so force shutdown
+                    executorService.shutdownNow();
+                }
+            }
+
+            // if we logged at WARN level, then report at INFO level when we are
+            // complete so the end user can see this in the log
+            if (warned) {
+                LOG.info("Shutdown of ExecutorService: {} is shutdown: {} and terminated: {} took: {}.",
+                    executorService, executorService.isShutdown(), executorService.isTerminated(), TimeUtils.printDuration(watch.taken()));
+            } else if (LOG.isDebugEnabled()) {
+                LOG.debug("Shutdown of ExecutorService: {} is shutdown: {} and terminated: {} took: {}.",
+                    executorService, executorService.isShutdown(), executorService.isTerminated(), TimeUtils.printDuration(watch.taken()));
+            }
+        }
+    }
+
+    /**
+     * Awaits the termination of the thread pool.
+     * <p>
+     * This implementation will log every 2nd second at INFO level that we are waiting, so the
+     * end user can see we are not hanging in case it takes longer time to terminate the pool.
+     *
+     * @param executorService
+     *        the thread pool
+     * @param shutdownAwaitTermination
+     *        time in millis to use as timeout
+     *
+     * @return <code>true</code> if the pool is terminated, or <code>false</code> if we timed out
+     *
+     * @throws InterruptedException
+     *         is thrown if we are interrupted during the waiting
+     */
+    public static boolean awaitTermination(ExecutorService executorService, long shutdownAwaitTermination) throws InterruptedException {
+
+        if (executorService == null) {
+            return true;
+        }
+
+        // log progress every 5th second so end user is aware of we are shutting down
+        StopWatch watch = new StopWatch();
+        long interval = Math.min(2000, shutdownAwaitTermination);
+        boolean done = false;
+        while (!done && interval > 0) {
+            if (executorService.awaitTermination(interval, TimeUnit.MILLISECONDS)) {
+                done = true;
+            } else {
+                LOG.debug("Waited {} for ExecutorService: {} to terminate...", TimeUtils.printDuration(watch.taken()), executorService);
+                // recalculate interval
+                interval = Math.min(2000, shutdownAwaitTermination - watch.taken());
+            }
+        }
+
+        return done;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/TimeUtils.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/TimeUtils.java
new file mode 100644
index 0000000..61edd14
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/TimeUtils.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Time utils.
+ */
+public final class TimeUtils {
+
+    private TimeUtils() {
+    }
+
+    /**
+     * Prints the duration in a human readable format as X days Y hours Z minutes etc.
+     *
+     * @param uptime
+     *        the uptime in millis
+     * @return the time used for displaying on screen or in logs
+     */
+    public static String printDuration(double uptime) {
+
+        NumberFormat fmtI = new DecimalFormat("###,###", new DecimalFormatSymbols(Locale.ENGLISH));
+        NumberFormat fmtD = new DecimalFormat("###,##0.000", new DecimalFormatSymbols(Locale.ENGLISH));
+
+        uptime /= 1000;
+        if (uptime < 60) {
+            return fmtD.format(uptime) + " seconds";
+        }
+        uptime /= 60;
+        if (uptime < 60) {
+            long minutes = (long) uptime;
+            String s = fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute");
+            return s;
+        }
+        uptime /= 60;
+        if (uptime < 24) {
+            long hours = (long) uptime;
+            long minutes = (long) ((uptime - hours) * 60);
+            String s = fmtI.format(hours) + (hours > 1 ? " hours" : " hour");
+            if (minutes != 0) {
+                s += " " + fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute");
+            }
+            return s;
+        }
+        uptime /= 24;
+        long days = (long) uptime;
+        long hours = (long) ((uptime - days) * 24);
+        String s = fmtI.format(days) + (days > 1 ? " days" : " day");
+        if (hours != 0) {
+            s += " " + fmtI.format(hours) + (hours > 1 ? " hours" : " hour");
+        }
+        return s;
+    }
+}
diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/TrackableThreadFactory.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/TrackableThreadFactory.java
new file mode 100644
index 0000000..760793a
--- /dev/null
+++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/util/TrackableThreadFactory.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Simple ThreadFactory object that tracks the last created thread if an {@link AtomicReference}
+ * is provided in order to hold onto the tracked {@link Thread} reference.
+ */
+public class TrackableThreadFactory implements ThreadFactory {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TrackableThreadFactory.class);
+
+    private final String threadName;
+    private final boolean daemon;
+    private final AtomicReference<Thread> threadTracker;
+
+    /**
+     * Creates a new Thread factory that will create threads with the
+     * given name and daemon state.
+     *
+     * @param threadName
+     *      the name that will be used for each thread created.
+     * @param daemon
+     *      should the created thread be a daemon thread.
+     */
+    public TrackableThreadFactory(String threadName, boolean daemon) {
+        this.threadName = threadName;
+        this.daemon = daemon;
+        this.threadTracker = null;
+    }
+
+    /**
+     * Creates a new Thread factory that will create threads with the
+     * given name and daemon state.
+     *
+     * This constructor accepts an AtomicReference to track the Thread that
+     * was last created from this factory.  This is most useful for a single
+     * threaded executor where the Id of the internal execution thread needs
+     * to be known for some reason.
+     *
+     * @param threadName
+     *      the name that will be used for each thread created.
+     * @param daemon
+     *      should the created thread be a daemon thread.
+     * @param threadTracker
+     *      AtomicReference that will be updated any time a new Thread is created.
+     */
+    public TrackableThreadFactory(String threadName, boolean daemon, AtomicReference<Thread> threadTracker) {
+        this.threadName = threadName;
+        this.daemon = daemon;
+        this.threadTracker = threadTracker;
+    }
+
+    @Override
+    public Thread newThread(final Runnable target) {
+        Runnable runner = target;
+
+        if (threadTracker != null) {
+            runner = new Runnable() {
+
+                @Override
+                public void run() {
+                    threadTracker.set(Thread.currentThread());
+
+                    try {
+                        target.run();
+                    } finally {
+                        threadTracker.set(null);
+                    }
+                }
+            };
+        }
+
+        Thread thread = new Thread(runner, threadName);
+        thread.setDaemon(daemon);
+        thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+
+            @Override
+            public void uncaughtException(Thread target, Throwable error) {
+                LOG.warn("Thread: {} failed due to an uncaught exception: {}", target.getName(), error.getMessage());
+                LOG.trace("Uncaught Stacktrace: ", error);
+            }
+        });
+
+        return thread;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/SaslOptionsTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/SaslOptionsTest.java
new file mode 100644
index 0000000..a8f0a7d
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/SaslOptionsTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class SaslOptionsTest {
+
+    @Test
+    void testCreate() {
+        SaslOptions options = new SaslOptions();
+
+        assertNotNull(options.allowedMechanisms());
+        assertTrue(options.allowedMechanisms().isEmpty());
+        assertTrue(options.saslEnabled());
+    }
+
+    @Test
+    void testCopy() {
+        SaslOptions options = new SaslOptions();
+
+        options.addAllowedMechanism("PLAIN");
+        options.addAllowedMechanism("ANONYMOUS");
+        options.saslEnabled(false);
+
+        SaslOptions copy = options.clone();
+
+        assertEquals(options.allowedMechanisms(), copy.allowedMechanisms());
+        assertEquals(options.saslEnabled(), copy.saslEnabled());
+    }
+
+    @Test
+    void testAllowedOptions() {
+        SaslOptions options = new SaslOptions();
+
+        assertNotNull(options.allowedMechanisms());
+        assertTrue(options.allowedMechanisms().isEmpty());
+
+        options.addAllowedMechanism("PLAIN");
+        options.addAllowedMechanism("ANONYMOUS");
+
+        assertFalse(options.allowedMechanisms().isEmpty());
+
+        options.allowedMechanisms().contains("PLAIN");
+        options.allowedMechanisms().contains("ANONYMOUS");
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/SslOptionsTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/SslOptionsTest.java
new file mode 100644
index 0000000..1f7fbf6
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/SslOptionsTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.mockito.Mockito;
+
+/**
+ * Tests for the {@link SslOptions} class
+ */
+public class SslOptionsTest extends ImperativeClientTestCase {
+
+    private static final String PASSWORD = "password";
+    private static final String CLIENT_KEYSTORE = "src/test/resources/client-jks.keystore";
+    private static final String CLIENT_TRUSTSTORE = "src/test/resources/client-jks.truststore";
+    private static final String KEYSTORE_TYPE = "jks";
+    private static final String KEY_ALIAS = "myTestAlias";
+    private static final String CONTEXT_PROTOCOL = "TLSv1.1";
+    private static final boolean TRUST_ALL = true;
+    private static final boolean VERIFY_HOST = true;
+
+    private static final int TEST_DEFAULT_SSL_PORT = 5681;
+    private static final boolean TEST_ALLOW_NATIVE_SSL = false;
+
+    private static final String[] ENABLED_PROTOCOLS = new String[] {"TLSv1.2"};
+    private static final String[] DISABLED_PROTOCOLS = new String[] {"SSLv3", "TLSv1.2"};
+    private static final String[] ENABLED_CIPHERS = new String[] {"CIPHER_A", "CIPHER_B"};
+    private static final String[] DISABLED_CIPHERS = new String[] {"CIPHER_C"};
+
+    private static final SSLContext SSL_CONTEXT = Mockito.mock(SSLContext.class);
+
+    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
+    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
+    private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword";
+
+    @Override
+    @AfterEach
+    public void tearDown(TestInfo testInfo) throws Exception {
+        super.tearDown(testInfo);
+    }
+
+    @Override
+    @BeforeEach
+    public void setUp(TestInfo testInfo) throws Exception {
+        super.setUp(testInfo);
+    }
+
+    @Test
+    public void testCreate() {
+        SslOptions options = new SslOptions();
+
+        assertFalse(options.sslEnabled());
+
+        assertEquals(SslOptions.DEFAULT_TRUST_ALL, options.trustAll());
+        assertEquals(SslOptions.DEFAULT_STORE_TYPE, options.keyStoreType());
+        assertEquals(SslOptions.DEFAULT_STORE_TYPE, options.trustStoreType());
+
+        assertEquals(SslOptions.DEFAULT_CONTEXT_PROTOCOL, options.contextProtocol());
+        assertNull(options.enabledProtocols());
+        assertArrayEquals(SslOptions.DEFAULT_DISABLED_PROTOCOLS.toArray(new String[0]), options.disabledProtocols());
+        assertNull(options.enabledCipherSuites());
+        assertNull(options.disabledCipherSuites());
+
+        assertNull(options.keyStoreLocation());
+        assertNull(options.keyStorePassword());
+        assertNull(options.trustStoreLocation());
+        assertNull(options.trustStorePassword());
+        assertNull(options.keyAlias());
+        assertNull(options.sslContextOverride());
+    }
+
+    @Test
+    public void testClone() {
+        SslOptions options = createNonDefaultOptions().clone();
+
+        assertTrue(options.sslEnabled());
+        assertEquals(TEST_DEFAULT_SSL_PORT, options.defaultSslPort());
+        assertEquals(CLIENT_KEYSTORE, options.keyStoreLocation());
+        assertEquals(PASSWORD, options.keyStorePassword());
+        assertEquals(CLIENT_TRUSTSTORE, options.trustStoreLocation());
+        assertEquals(PASSWORD, options.trustStorePassword());
+        assertEquals(KEYSTORE_TYPE, options.keyStoreType());
+        assertEquals(KEYSTORE_TYPE, options.trustStoreType());
+        assertEquals(KEY_ALIAS, options.keyAlias());
+        assertEquals(CONTEXT_PROTOCOL, options.contextProtocol());
+        assertEquals(SSL_CONTEXT, options.sslContextOverride());
+        assertArrayEquals(ENABLED_PROTOCOLS,options.enabledProtocols());
+        assertArrayEquals(DISABLED_PROTOCOLS,options.disabledProtocols());
+        assertArrayEquals(ENABLED_CIPHERS,options.enabledCipherSuites());
+        assertArrayEquals(DISABLED_CIPHERS,options.disabledCipherSuites());
+    }
+
+    @Test
+    public void testCreateAndConfigure() {
+        SslOptions options = createNonDefaultOptions();
+
+        assertEquals(CLIENT_KEYSTORE, options.keyStoreLocation());
+        assertEquals(PASSWORD, options.keyStorePassword());
+        assertEquals(CLIENT_TRUSTSTORE, options.trustStoreLocation());
+        assertEquals(PASSWORD, options.trustStorePassword());
+        assertEquals(KEYSTORE_TYPE, options.keyStoreType());
+        assertEquals(KEYSTORE_TYPE, options.trustStoreType());
+        assertEquals(KEY_ALIAS, options.keyAlias());
+        assertEquals(CONTEXT_PROTOCOL, options.contextProtocol());
+        assertEquals(SSL_CONTEXT, options.sslContextOverride());
+        assertArrayEquals(ENABLED_PROTOCOLS,options.enabledProtocols());
+        assertArrayEquals(DISABLED_PROTOCOLS,options.disabledProtocols());
+        assertArrayEquals(ENABLED_CIPHERS,options.enabledCipherSuites());
+        assertArrayEquals(DISABLED_CIPHERS,options.disabledCipherSuites());
+    }
+
+    @Test
+    public void testSslSystemPropertiesInfluenceDefaults() {
+        String keystore = "keystore";
+        String keystorePass = "keystorePass";
+        String truststore = "truststore";
+        String truststorePass = "truststorePass";
+
+        setSslSystemPropertiesForCurrentTest(keystore, keystorePass, truststore, truststorePass);
+
+        SslOptions options1 = new SslOptions();
+
+        assertEquals(keystore, options1.keyStoreLocation());
+        assertEquals(keystorePass, options1.keyStorePassword());
+        assertEquals(truststore, options1.trustStoreLocation());
+        assertEquals(truststorePass, options1.trustStorePassword());
+
+        keystore +="2";
+        keystorePass +="2";
+        truststore +="2";
+        truststorePass +="2";
+
+        setSslSystemPropertiesForCurrentTest(keystore, keystorePass, truststore, truststorePass);
+
+        SslOptions options2 = new SslOptions();
+
+        assertEquals(keystore, options2.keyStoreLocation());
+        assertEquals(keystorePass, options2.keyStorePassword());
+        assertEquals(truststore, options2.trustStoreLocation());
+        assertEquals(truststorePass, options2.trustStorePassword());
+
+        assertNotEquals(options1.keyStoreLocation(), options2.keyStoreLocation());
+        assertNotEquals(options1.keyStorePassword(), options2.keyStorePassword());
+        assertNotEquals(options1.trustStoreLocation(), options2.trustStoreLocation());
+        assertNotEquals(options1.trustStorePassword(), options2.trustStorePassword());
+    }
+
+    private void setSslSystemPropertiesForCurrentTest(String keystore, String keystorePassword, String truststore, String truststorePassword) {
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE, keystore);
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD, keystorePassword);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE, truststore);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD, truststorePassword);
+    }
+
+    private SslOptions createNonDefaultOptions() {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.defaultSslPort(TEST_DEFAULT_SSL_PORT);
+        options.allowNativeSSL(TEST_ALLOW_NATIVE_SSL);
+        options.keyStoreLocation(CLIENT_KEYSTORE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStoreLocation(CLIENT_TRUSTSTORE);
+        options.trustStorePassword(PASSWORD);
+        options.trustAll(TRUST_ALL);
+        options.verifyHost(VERIFY_HOST);
+        options.keyAlias(KEY_ALIAS);
+        options.contextProtocol(CONTEXT_PROTOCOL);
+        options.sslContextOverride(SSL_CONTEXT);
+        options.enabledProtocols(ENABLED_PROTOCOLS);
+        options.enabledCipherSuites(ENABLED_CIPHERS);
+        options.disabledProtocols(DISABLED_PROTOCOLS);
+        options.disabledCipherSuites(DISABLED_CIPHERS);
+
+        return options;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/TransportOptionsTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/TransportOptionsTest.java
new file mode 100644
index 0000000..04de894
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/TransportOptionsTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for class TransportOptions
+ */
+public class TransportOptionsTest extends ImperativeClientTestCase {
+
+    public static final int TEST_SEND_BUFFER_SIZE = 128 * 1024;
+    public static final int TEST_RECEIVE_BUFFER_SIZE = TEST_SEND_BUFFER_SIZE;
+    public static final int TEST_TRAFFIC_CLASS = 1;
+    public static final boolean TEST_TCP_NO_DELAY = false;
+    public static final boolean TEST_TCP_KEEP_ALIVE = true;
+    public static final int TEST_SO_LINGER = Short.MAX_VALUE;
+    public static final int TEST_SO_TIMEOUT = 10;
+    public static final int TEST_CONNECT_TIMEOUT = 90000;
+    public static final int TEST_DEFAULT_TCP_PORT = 5682;
+    public static final String LOCAL_ADDRESS = "localhost";
+    public static final int LOCAL_PORT = 30000;
+    public static final boolean TEST_ALLOW_NATIVE_IO_VALUE = !TransportOptions.DEFAULT_ALLOW_NATIVE_IO;
+    public static final boolean TEST_TRACE_BYTES_VALUE = !TransportOptions.DEFAULT_TRACE_BYTES;
+    public static final String TEST_WEBSOCKET_PATH = "/test";
+    public static final String TEST_WEBSOCKET_HEADER_KEY = "compression";
+    public static final String TEST_WEBSOCKET_HEADER_VALUE = "gzip";
+    public static final int TEST_WEBSOCKET_MAX_FRAME_SIZE = TransportOptions.DEFAULT_WEBSOCKET_MAX_FRAME_SIZE + 1024;
+
+    @Test
+    public void testCreate() {
+        TransportOptions options = new TransportOptions();
+
+        assertEquals(TransportOptions.DEFAULT_TCP_NO_DELAY, options.tcpNoDelay());
+
+        assertTrue(options.allowNativeIO());
+        assertFalse(options.useWebSockets());
+        assertNull(options.webSocketPath());
+    }
+
+    @Test
+    public void testOptions() {
+        TransportOptions options = createNonDefaultOptions();
+
+        assertEquals(TEST_SEND_BUFFER_SIZE, options.sendBufferSize());
+        assertEquals(TEST_RECEIVE_BUFFER_SIZE, options.receiveBufferSize());
+        assertEquals(TEST_TRAFFIC_CLASS, options.trafficClass());
+        assertEquals(TEST_TCP_NO_DELAY, options.tcpNoDelay());
+        assertEquals(TEST_TCP_KEEP_ALIVE, options.tcpKeepAlive());
+        assertEquals(TEST_SO_LINGER, options.soLinger());
+        assertEquals(TEST_SO_TIMEOUT, options.soTimeout());
+        assertEquals(TEST_CONNECT_TIMEOUT, options.connectTimeout());
+        assertEquals(TEST_DEFAULT_TCP_PORT, options.defaultTcpPort());
+        assertEquals(TEST_ALLOW_NATIVE_IO_VALUE, options.allowNativeIO());
+        assertEquals(TEST_TRACE_BYTES_VALUE, options.traceBytes());
+    }
+
+    @Test
+    public void testClone() {
+        TransportOptions options = createNonDefaultOptions().clone();
+
+        assertEquals(TEST_SEND_BUFFER_SIZE, options.sendBufferSize());
+        assertEquals(TEST_RECEIVE_BUFFER_SIZE, options.receiveBufferSize());
+        assertEquals(TEST_TRAFFIC_CLASS, options.trafficClass());
+        assertEquals(TEST_TCP_NO_DELAY, options.tcpNoDelay());
+        assertEquals(TEST_TCP_KEEP_ALIVE, options.tcpKeepAlive());
+        assertEquals(TEST_SO_LINGER, options.soLinger());
+        assertEquals(TEST_SO_TIMEOUT, options.soTimeout());
+        assertEquals(TEST_CONNECT_TIMEOUT, options.connectTimeout());
+        assertEquals(TEST_DEFAULT_TCP_PORT, options.defaultTcpPort());
+        assertEquals(TEST_ALLOW_NATIVE_IO_VALUE, options.allowNativeIO());
+        assertEquals(TEST_TRACE_BYTES_VALUE, options.traceBytes());
+        assertEquals(LOCAL_ADDRESS,options.localAddress());
+        assertEquals(LOCAL_PORT,options.localPort());
+        assertEquals(TEST_WEBSOCKET_PATH, options.webSocketPath());
+        assertEquals(TEST_WEBSOCKET_HEADER_VALUE, options.webSocketHeaders().get(TEST_WEBSOCKET_HEADER_KEY));
+        assertEquals(TEST_WEBSOCKET_MAX_FRAME_SIZE, options.webSocketMaxFrameSize());
+    }
+
+    @Test
+    public void testSendBufferSizeValidation() {
+        TransportOptions options = createNonDefaultOptions().clone();
+        try {
+            options.sendBufferSize(0);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+        try {
+            options.sendBufferSize(-1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        options.sendBufferSize(1);
+    }
+
+    @Test
+    public void testReceiveBufferSizeValidation() {
+        TransportOptions options = createNonDefaultOptions().clone();
+        try {
+            options.receiveBufferSize(0);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+        try {
+            options.receiveBufferSize(-1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        options.receiveBufferSize(1);
+    }
+
+    @Test
+    public void testTrafficClassValidation() {
+        TransportOptions options = createNonDefaultOptions().clone();
+        try {
+            options.trafficClass(-1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+        try {
+            options.trafficClass(256);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        options.trafficClass(0);
+        options.trafficClass(128);
+        options.trafficClass(255);
+    }
+
+    @Test
+    public void testNativeIOPerferencesCannotBeNulled() {
+        TransportOptions options = createNonDefaultOptions();
+
+        assertNotNull(options.nativeIOPeference());
+        assertArrayEquals(TransportOptions.DEFAULT_NATIVEIO_PREFERENCES, options.nativeIOPeference());
+
+        options.nativeIOPeference((String) null);
+
+        assertNotNull(options.nativeIOPeference());
+        assertArrayEquals(TransportOptions.DEFAULT_NATIVEIO_PREFERENCES, options.nativeIOPeference());
+
+        options.nativeIOPeference("epolling");
+
+        assertNotNull(options.nativeIOPeference());
+        assertArrayEquals(new String[] { "epolling" }, options.nativeIOPeference());
+    }
+
+    @Test
+    public void testCreateAndConfigure() {
+        TransportOptions options = createNonDefaultOptions();
+
+        assertEquals(TEST_SEND_BUFFER_SIZE, options.sendBufferSize());
+        assertEquals(TEST_RECEIVE_BUFFER_SIZE, options.receiveBufferSize());
+        assertEquals(TEST_TRAFFIC_CLASS, options.trafficClass());
+        assertEquals(TEST_TCP_NO_DELAY, options.tcpNoDelay());
+        assertEquals(TEST_TCP_KEEP_ALIVE, options.tcpKeepAlive());
+        assertEquals(TEST_SO_LINGER, options.soLinger());
+        assertEquals(TEST_SO_TIMEOUT, options.soTimeout());
+        assertEquals(TEST_CONNECT_TIMEOUT, options.connectTimeout());
+    }
+
+    private TransportOptions createNonDefaultOptions() {
+        TransportOptions options = new TransportOptions();
+
+        options.sendBufferSize(TEST_SEND_BUFFER_SIZE);
+        options.receiveBufferSize(TEST_RECEIVE_BUFFER_SIZE);
+        options.trafficClass(TEST_TRAFFIC_CLASS);
+        options.tcpNoDelay(TEST_TCP_NO_DELAY);
+        options.tcpKeepAlive(TEST_TCP_KEEP_ALIVE);
+        options.soLinger(TEST_SO_LINGER);
+        options.soTimeout(TEST_SO_TIMEOUT);
+        options.connectTimeout(TEST_CONNECT_TIMEOUT);
+        options.defaultTcpPort(TEST_DEFAULT_TCP_PORT);
+        options.allowNativeIO(TEST_ALLOW_NATIVE_IO_VALUE);
+        options.traceBytes(TEST_TRACE_BYTES_VALUE);
+        options.localAddress(LOCAL_ADDRESS);
+        options.localPort(LOCAL_PORT);
+        options.webSocketPath(TEST_WEBSOCKET_PATH);
+        options.addWebSocketHeader(TEST_WEBSOCKET_HEADER_KEY, TEST_WEBSOCKET_HEADER_VALUE);
+        options.webSocketMaxFrameSize(TEST_WEBSOCKET_MAX_FRAME_SIZE);
+
+        return options;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/ClientFutureFactoryTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/ClientFutureFactoryTest.java
new file mode 100644
index 0000000..bbead5b
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/ClientFutureFactoryTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class ClientFutureFactoryTest {
+
+    @Test
+    public void testCreateFailsWithNullOptions() {
+        ClientFutureFactory factory = ClientFutureFactory.create(null);
+
+        ClientFuture<Void> future = factory.createFuture();
+        assertNotNull(future);
+        assertFalse(future.isComplete());
+    }
+
+    @Test
+    public void testCreateFailsWhenFutureTypeNotValid() {
+        try {
+            ClientFutureFactory.create("super-fast");
+            fail("Should throw IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testCreateFactoryWithNoConfigurationOptionsGiven() {
+        ClientFutureFactory factory = ClientFutureFactory.create("");
+
+        ClientFuture<Void> future = factory.createFuture();
+        assertNotNull(future);
+        assertFalse(future.isComplete());
+    }
+
+    @Test
+    public void testCreateConservativeFactoryFromConfiguration() {
+        ClientFutureFactory factory = ClientFutureFactory.create("conservative");
+
+        ClientFuture<Void> future = factory.createFuture();
+        assertNotNull(future);
+        assertFalse(future.isComplete());
+
+        assertTrue(future instanceof ConservativeClientFuture);
+    }
+
+    @Test
+    public void testCreateBalancedFactoryFromConfiguration() {
+        ClientFutureFactory factory = ClientFutureFactory.create("balanced");
+
+        ClientFuture<Void> future = factory.createFuture();
+        assertNotNull(future);
+        assertFalse(future.isComplete());
+
+        assertTrue(future instanceof BalancedClientFuture);
+    }
+
+    @Test
+    public void testCreateProgressiveFactoryFromConfiguration() {
+        ClientFutureFactory factory = ClientFutureFactory.create("progressive");
+
+        ClientFuture<Void> future = factory.createFuture();
+        assertNotNull(future);
+        assertFalse(future.isComplete());
+
+        assertTrue(future instanceof ProgressiveClientFuture);
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/ClientFutureTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/ClientFutureTest.java
new file mode 100644
index 0000000..c64d3ce
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/ClientFutureTest.java
@@ -0,0 +1,568 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.security.ProviderException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@Timeout(20)
+public class ClientFutureTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testIsComplete(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        assertFalse(future.isComplete());
+        assertFalse(future.isDone());
+        assertFalse(future.isFailed());
+
+        future.complete(null);
+
+        assertFalse(future.isFailed());
+        assertTrue(future.isDone());
+        assertTrue(future.isComplete());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnSuccess(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Boolean> future = futuresFactory.createFuture();
+
+        assertFalse(future.isComplete());
+        assertFalse(future.isDone());
+
+        future.complete(true);
+
+        try {
+            assertTrue(future.get());
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(future.isComplete());
+        assertTrue(future.isDone());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnSuccessFromAnotherThread(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Boolean> future = futuresFactory.createFuture();
+
+        assertFalse(future.isComplete());
+        assertFalse(future.isDone());
+
+        ForkJoinPool.commonPool().submit(() -> future.complete(true));
+
+        try {
+            assertTrue(future.get());
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(future.isComplete());
+        assertTrue(future.isDone());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedGet(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        try {
+            assertNull(future.get(500, TimeUnit.MILLISECONDS));
+        } catch (TimeoutException cause) {
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedGetWhenComplete(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        future.complete(null);
+
+        try {
+            assertNull(future.get(500, TimeUnit.MILLISECONDS));
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedGetWhenCompleteWithZeroTimeout(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        future.complete(null);
+
+        try {
+            assertNull(future.get(0, TimeUnit.MILLISECONDS));
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedGetWhenNotCompleteWithZeroTimeout(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        try {
+            assertNull(future.get(0, TimeUnit.MILLISECONDS));
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedGetWhenCancelled(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        future.cancel(true);
+
+        try {
+            assertNull(future.get(10, TimeUnit.MILLISECONDS));
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(future.isCancelled());
+        assertTrue(future.isDone());
+        assertFalse(future.isFailed());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedGetWhenCancelledFromAnotherThread(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        ForkJoinPool.commonPool().submit(() -> future.cancel(true));
+
+        try {
+            assertNull(future.get(10, TimeUnit.MILLISECONDS));
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(future.isCancelled());
+        assertTrue(future.isDone());
+        assertFalse(future.isFailed());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnFailure(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+        final ClientException ex = new ClientException("Failed");
+
+        future.failed(ex);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+            fail("Should throw an error");
+        } catch (ExecutionException exe) {
+            assertSame(exe.getCause(), ex);
+        } catch (InterruptedException e) {
+            fail("Should not throw an ExecutionException");
+        } catch (TimeoutException e) {
+            fail("Should not throw an ExecutionException");
+        }
+
+        assertTrue(future.isDone());
+        assertTrue(future.isFailed());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnFailureFromAnotherThread(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+        final ClientException ex = new ClientException("Failed");
+
+        ForkJoinPool.commonPool().submit(() -> future.failed(ex));
+
+        try {
+            future.get(5, TimeUnit.SECONDS);
+            fail("Should throw an error");
+        } catch (ExecutionException exe) {
+            assertSame(exe.getCause(), ex);
+        } catch (InterruptedException e) {
+            fail("Should not throw an ExecutionException");
+        } catch (TimeoutException e) {
+            fail("Should not throw an ExecutionException");
+        }
+
+        assertTrue(future.isDone());
+        assertTrue(future.isFailed());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnSuccessCallsSynchronization(String futureType) {
+        final AtomicBoolean syncCalled = new AtomicBoolean(false);
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+
+        final ClientFuture<Void> future = futuresFactory.createFuture(new ClientSynchronization<Void>() {
+
+            @Override
+            public void onPendingSuccess(Void result) {
+                syncCalled.set(true);
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+
+            }
+        });
+
+        future.complete(null);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(syncCalled.get(), "Synchronization not called");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnFailureCallsSynchronization(String futureType) {
+        final AtomicBoolean syncCalled = new AtomicBoolean(false);
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+
+        final ClientFuture<Void> future = futuresFactory.createFuture(new ClientSynchronization<Void>() {
+
+            @Override
+            public void onPendingSuccess(Void result) {
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+                syncCalled.set(true);
+            }
+        });
+
+        final ClientException ex = new ClientException("Failed");
+
+        future.failed(ex);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+            fail("Should throw an error");
+        } catch (ProviderException cause) {
+        } catch (InterruptedException e) {
+            fail("Should not throw an ExecutionException");
+        } catch (ExecutionException e) {
+            assertSame(e.getCause(), ex);
+        } catch (TimeoutException e) {
+            fail("Should not throw an ExecutionException");
+        }
+
+        assertTrue(syncCalled.get(), "Synchronization not called");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnSuccessCallsSynchronizationIngoresThrownError(String futureType) {
+        final AtomicBoolean syncCalled = new AtomicBoolean(false);
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+
+        final ClientFuture<Void> future = futuresFactory.createFuture(new ClientSynchronization<Void>() {
+
+            @Override
+            public void onPendingSuccess(Void result) {
+                syncCalled.set(true);
+                throw new RuntimeException();
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+
+            }
+        });
+
+        future.complete(null);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(syncCalled.get(), "Synchronization not called");
+        assertTrue(future.isComplete());
+        assertTrue(future.isDone());
+        assertFalse(future.isFailed());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testOnFailureCallsSynchronizationAndIngoresThrownErrors(String futureType) {
+        final AtomicBoolean syncCalled = new AtomicBoolean(false);
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+
+        final ClientFuture<Void> future = futuresFactory.createFuture(new ClientSynchronization<Void>() {
+
+            @Override
+            public void onPendingSuccess(Void result) {
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+                syncCalled.set(true);
+                throw new RuntimeException();
+            }
+        });
+
+        final ClientException ex = new ClientException("Failed");
+
+        future.failed(ex);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+            fail("Should throw an error");
+        } catch (ProviderException cause) {
+        } catch (InterruptedException e) {
+            fail("Should not throw an ExecutionException");
+        } catch (ExecutionException e) {
+            assertSame(e.getCause(), ex);
+        } catch (TimeoutException e) {
+            fail("Should not throw an ExecutionException");
+        }
+
+        assertTrue(syncCalled.get(), "Synchronization not called");
+        assertTrue(future.isComplete());
+        assertTrue(future.isDone());
+        assertTrue(future.isFailed());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testSuccessfulStateIsFixed(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+        final ClientException ex = new ClientException("Failed");
+
+        future.complete(null);
+        future.failed(ex);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testFailedStateIsFixed(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+        final ClientException ex = new ClientException("Failed");
+
+        future.failed(ex);
+        future.complete(null);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+            fail("Should throw an error");
+        } catch (InterruptedException e) {
+            fail("Should have thrown an execution exception");
+        } catch (ExecutionException e) {
+            assertSame(e.getCause(), ex);
+        } catch (TimeoutException e) {
+            fail("Should have thrown an execution exception");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testSyncHandlesInterruption(String futureType) throws InterruptedException {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        final CountDownLatch syncing = new CountDownLatch(1);
+        final CountDownLatch done = new CountDownLatch(1);
+        final AtomicBoolean interrupted = new AtomicBoolean(false);
+
+        Thread runner = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    syncing.countDown();
+                    future.get();
+                } catch (InterruptedException cause) {
+                    Thread.interrupted();
+                    interrupted.set(true);
+                } catch (ExecutionException e) {
+                } finally {
+                    done.countDown();
+                }
+            }
+        });
+
+        runner.start();
+        assertTrue(syncing.await(5, TimeUnit.SECONDS));
+        runner.interrupt();
+
+        assertTrue(done.await(5, TimeUnit.SECONDS));
+
+        assertTrue(interrupted.get());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testTimedSyncHandlesInterruption(String futureType) throws InterruptedException {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createFuture();
+
+        final CountDownLatch syncing = new CountDownLatch(1);
+        final CountDownLatch done = new CountDownLatch(1);
+        final AtomicBoolean interrupted = new AtomicBoolean(false);
+
+        Thread runner = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    syncing.countDown();
+                    future.get(20, TimeUnit.SECONDS);
+                } catch (InterruptedException cause) {
+                    Thread.interrupted();
+                    interrupted.set(true);
+                } catch (ExecutionException e) {
+                } catch (TimeoutException e) {
+                } finally {
+                    done.countDown();
+                }
+            }
+        });
+
+        runner.start();
+        assertTrue(syncing.await(5, TimeUnit.SECONDS));
+        runner.interrupt();
+
+        assertTrue(done.await(5, TimeUnit.SECONDS));
+
+        assertTrue(interrupted.get());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testUnfailableOnSuccessCallsSuccessSynchronization(String futureType) {
+        final AtomicBoolean syncCalled = new AtomicBoolean(false);
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+
+        final ClientFuture<Void> future = futuresFactory.createUnfailableFuture(new ClientSynchronization<Void>() {
+
+            @Override
+            public void onPendingSuccess(Void result) {
+                syncCalled.set(true);
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+
+            }
+        });
+
+        future.complete(null);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(syncCalled.get(), "Synchronization not called");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testUnfailableOnFailureCannotFail(String futureType) {
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+        final ClientFuture<Void> future = futuresFactory.createUnfailableFuture();
+        final ClientException ex = new ClientException("Failed");
+
+        future.failed(ex);
+
+        try {
+            future.get(5, TimeUnit.SECONDS);
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(future.isDone());
+        assertFalse(future.isFailed());
+        assertFalse(future.isCancelled());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "conservative", "balanced", "progressive" })
+    public void testUnfailableOnFailureCallsSuccessSynchronizationWhenFailed(String futureType) {
+        final AtomicBoolean syncCalled = new AtomicBoolean(false);
+        final ClientFutureFactory futuresFactory = ClientFutureFactory.create(futureType);
+
+        final ClientFuture<Void> future = futuresFactory.createUnfailableFuture(new ClientSynchronization<Void>() {
+
+            @Override
+            public void onPendingSuccess(Void result) {
+                syncCalled.set(true);
+            }
+
+            @Override
+            public void onPendingFailure(Throwable cause) {
+            }
+        });
+
+        final ClientException ex = new ClientException("Failed");
+
+        future.failed(ex);
+        try {
+            future.get(5, TimeUnit.SECONDS);
+        } catch (Exception cause) {
+            fail("Should not throw an error");
+        }
+
+        assertTrue(syncCalled.get(), "Synchronization not called");
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/NoOpAsyncResultTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/NoOpAsyncResultTest.java
new file mode 100644
index 0000000..ca423f8
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/futures/NoOpAsyncResultTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.futures;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.junit.jupiter.api.Test;
+
+public class NoOpAsyncResultTest {
+
+    @Test
+    public void testDefaultToComplete() {
+        NoOpAsyncResult result = new NoOpAsyncResult();
+        assertTrue(result.isComplete());
+    }
+
+    @Test
+    public void testOnSuccess() {
+        NoOpAsyncResult result = new NoOpAsyncResult();
+
+        assertTrue(result.isComplete());
+        result.complete(null);
+        result.failed(new ClientException("Error"));
+        assertTrue(result.isComplete());
+    }
+
+    @Test
+    public void testOnFailure() {
+        NoOpAsyncResult result = new NoOpAsyncResult();
+
+        assertTrue(result.isComplete());
+        result.failed(new ClientException("Error"));
+        result.complete(null);
+        assertTrue(result.isComplete());
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientConnectionCapabilitiesTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientConnectionCapabilitiesTest.java
new file mode 100644
index 0000000..cc3184c
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientConnectionCapabilitiesTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class ClientConnectionCapabilitiesTest {
+
+    public static final Symbol[] ANONYMOUS_RELAY = new Symbol[] { Symbol.valueOf("ANONYMOUS-RELAY") };
+    public static final Symbol[] DELAYED_DELIVERY = new Symbol[] { Symbol.valueOf("DELAYED_DELIVERY") };
+    public static final Symbol[] ANONYMOUS_RELAY_PLUS = new Symbol[] { Symbol.valueOf("ANONYMOUS-RELAY"),
+                                                                       Symbol.valueOf("DELAYED_DELIVERY")};
+
+    @Test
+    void testAnonymousRelaySupportedIsFalseByDefault() {
+        ClientConnectionCapabilities capabilities = new ClientConnectionCapabilities();
+
+        assertFalse(capabilities.anonymousRelaySupported());
+    }
+
+    @Test
+    public void testAnonymousRelaySupportedWhenBothIndicateInCapabilities() {
+        doTestIsAnonymousRelaySupported(ANONYMOUS_RELAY, ANONYMOUS_RELAY, true);
+    }
+
+    @Test
+    public void testAnonymousRelaySupportedWhenBothIndicateInCapabilitiesAlongWithOthers() {
+        doTestIsAnonymousRelaySupported(ANONYMOUS_RELAY, ANONYMOUS_RELAY_PLUS, true);
+    }
+
+    @Test
+    public void testAnonymousRelayNotSupportedWhenServerDoesNotAdvertiseIt() {
+        doTestIsAnonymousRelaySupported(ANONYMOUS_RELAY, null, false);
+    }
+
+    @Test
+    public void testAnonymousRelaySupportedWhenServerAdvertisesButClientDoesNotRequestIt() {
+        doTestIsAnonymousRelaySupported(null, ANONYMOUS_RELAY, true);
+    }
+
+    @Test
+    public void testAnonymousRelayNotSupportedWhenNeitherSideIndicatesIt() {
+        doTestIsAnonymousRelaySupported(null, null, false);
+    }
+
+    private void doTestIsAnonymousRelaySupported(Symbol[] desired, Symbol[] offered, boolean expectation) {
+        ClientConnectionCapabilities capabilities = new ClientConnectionCapabilities();
+
+        Connection connection = Mockito.mock(Connection.class);
+        Mockito.when(connection.getDesiredCapabilities()).thenReturn(desired);
+        Mockito.when(connection.getRemoteOfferedCapabilities()).thenReturn(offered);
+
+        capabilities.determineCapabilities(connection);
+
+        if (expectation) {
+            assertTrue(capabilities.anonymousRelaySupported());
+        } else {
+            assertFalse(capabilities.anonymousRelaySupported());
+        }
+    }
+
+    @Test
+    public void testDelayedDeliverySupportedWhenBothIndicateInCapabilities() {
+        doTestIsDelayedDeliverySupported(DELAYED_DELIVERY, DELAYED_DELIVERY, true);
+    }
+
+    @Test
+    public void testDelayedDeliverySupportedWhenBothIndicateInCapabilitiesAlongWithOthers() {
+        doTestIsDelayedDeliverySupported(DELAYED_DELIVERY, ANONYMOUS_RELAY_PLUS, true);
+    }
+
+    @Test
+    public void testDelayedDeliveryNotSupportedWhenServerDoesNotAdvertiseIt() {
+        doTestIsDelayedDeliverySupported(DELAYED_DELIVERY, null, false);
+    }
+
+    @Test
+    public void testDelayedDeliverySupportedWhenServerAdvertisesButClientDoesNotRequestIt() {
+        doTestIsDelayedDeliverySupported(null, DELAYED_DELIVERY, true);
+    }
+
+    @Test
+    public void testDelayedDeliveryNotSupportedWhenNeitherSideIndicatesIt() {
+        doTestIsDelayedDeliverySupported(null, null, false);
+    }
+
+    private void doTestIsDelayedDeliverySupported(Symbol[] desired, Symbol[] offered, boolean expectation) {
+        ClientConnectionCapabilities capabilities = new ClientConnectionCapabilities();
+
+        Connection connection = Mockito.mock(Connection.class);
+        Mockito.when(connection.getDesiredCapabilities()).thenReturn(desired);
+        Mockito.when(connection.getRemoteOfferedCapabilities()).thenReturn(offered);
+
+        capabilities.determineCapabilities(connection);
+
+        if (expectation) {
+            assertTrue(capabilities.deliveryDelaySupported());
+        } else {
+            assertFalse(capabilities.deliveryDelaySupported());
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientErrorConditionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientErrorConditionTest.java
new file mode 100644
index 0000000..73c6801
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientErrorConditionTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+class ClientErrorConditionTest {
+
+    @Test
+    void testCreateWithNullProtonError() {
+        assertNull(ClientErrorCondition.asProtonErrorCondition(null));
+    }
+
+    @Test
+    void testCreateWithErrorConditionThatOnlyHasConditionData() {
+        ErrorCondition condition = ErrorCondition.create("amqp:error", null);
+        ClientErrorCondition clientCondition = new ClientErrorCondition(condition);
+
+        assertEquals("amqp:error", clientCondition.condition());
+        assertNull(clientCondition.description());
+        assertNotNull(clientCondition.info());
+
+        org.apache.qpid.protonj2.types.transport.ErrorCondition protonError = clientCondition.getProtonErrorCondition();
+
+        assertNotNull(protonError);
+        assertEquals("amqp:error", protonError.getCondition().toString());
+        assertNull(protonError.getDescription());
+        assertNotNull(protonError.getInfo());
+    }
+
+    @Test
+    void testCreateWithErrorConditionWithConditionAndDescription() {
+        ErrorCondition condition = ErrorCondition.create("amqp:error", "example");
+        ClientErrorCondition clientCondition = new ClientErrorCondition(condition);
+
+        assertEquals("amqp:error", clientCondition.condition());
+        assertEquals("example", clientCondition.description());
+        assertNotNull(clientCondition.info());
+
+        org.apache.qpid.protonj2.types.transport.ErrorCondition protonError = clientCondition.getProtonErrorCondition();
+
+        assertNotNull(protonError);
+        assertEquals("amqp:error", protonError.getCondition().toString());
+        assertEquals("example", protonError.getDescription());
+        assertNotNull(protonError.getInfo());
+    }
+
+    @Test
+    void testCreateWithErrorConditionWithConditionAndDescriptionAndInfo() {
+        Map<String, Object> infoMap = new HashMap<>();
+        infoMap.put("test", "value");
+
+        ErrorCondition condition = ErrorCondition.create("amqp:error", "example", infoMap);
+        ClientErrorCondition clientCondition = new ClientErrorCondition(condition);
+
+        assertEquals("amqp:error", clientCondition.condition());
+        assertEquals("example", clientCondition.description());
+        assertEquals(infoMap, clientCondition.info());
+
+        org.apache.qpid.protonj2.types.transport.ErrorCondition protonError = clientCondition.getProtonErrorCondition();
+
+        assertNotNull(protonError);
+        assertEquals("amqp:error", protonError.getCondition().toString());
+        assertEquals("example", protonError.getDescription());
+        assertEquals(infoMap, ClientConversionSupport.toStringKeyedMap(protonError.getInfo()));
+    }
+
+    @Test
+    void testCreateFromForeignErrorCondition() {
+        Map<String, Object> infoMap = new HashMap<>();
+        infoMap.put("test", "value");
+
+        ErrorCondition condition = new ErrorCondition() {
+
+            @Override
+            public Map<String, Object> info() {
+                return infoMap;
+            }
+
+            @Override
+            public String description() {
+                return "example";
+            }
+
+            @Override
+            public String condition() {
+                return "amqp:error";
+            }
+        };
+
+        org.apache.qpid.protonj2.types.transport.ErrorCondition protonError = ClientErrorCondition.asProtonErrorCondition(condition);
+
+        assertNotNull(protonError);
+        assertEquals("amqp:error", protonError.getCondition().toString());
+        assertEquals("example", protonError.getDescription());
+        assertEquals(infoMap, ClientConversionSupport.toStringKeyedMap(protonError.getInfo()));
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientExceptionSupportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientExceptionSupportTest.java
new file mode 100644
index 0000000..d5e29af
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientExceptionSupportTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.TimeoutException;
+
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.junit.jupiter.api.Test;
+
+class ClientExceptionSupportTest {
+
+    @Test
+    void testClientIOExceptionPassesThrough() {
+        ClientIOException ioError = new ClientIOException("Fatal IO Error");
+
+        assertSame(ioError, ClientExceptionSupport.createOrPassthroughFatal(ioError));
+    }
+
+    @Test
+    void testClientExceptionNotPassesThrough() {
+        ClientException error = new ClientException("Fatal IO Error");
+
+        assertNotSame(error, ClientExceptionSupport.createOrPassthroughFatal(error));
+        assertTrue(ClientExceptionSupport.createOrPassthroughFatal(error) instanceof ClientIOException);
+    }
+
+    @Test
+    void testErrorMessageTakenFromToStringIfNotPresentInException() {
+        Throwable error = new Exception() {
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public String toString() {
+                return "expected";
+            }
+        };
+
+        assertNotSame(error, ClientExceptionSupport.createOrPassthroughFatal(error));
+        assertEquals("expected", ClientExceptionSupport.createOrPassthroughFatal(error).getMessage());
+    }
+
+    @Test
+    void testErrorMessageTakenFromToStringIfEmptyInException() {
+        Throwable error = new Exception("") {
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public String toString() {
+                return "expected";
+            }
+        };
+
+        assertNotSame(error, ClientExceptionSupport.createOrPassthroughFatal(error));
+        assertEquals("expected", ClientExceptionSupport.createOrPassthroughFatal(error).getMessage());
+    }
+
+    @Test
+    void testCauseIsClientIOExceptionExtractedAndPassedThrough() {
+        ClientException error = new ClientException("Fatal IO Error", new ClientIOException("real error"));
+
+        assertNotSame(error, ClientExceptionSupport.createOrPassthroughFatal(error));
+        assertSame(error.getCause(), ClientExceptionSupport.createOrPassthroughFatal(error));
+    }
+
+    @Test
+    void testClientExceptionPassesThrough() {
+        ClientIOException error = new ClientIOException("Non Fatal Error");
+
+        assertSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+    }
+
+    @Test
+    void testClientIOExceptionPassesThroughNonFatalCreate() {
+        ClientIOException error = new ClientIOException("Fatal IO Error");
+
+        assertSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+    }
+
+    @Test
+    void testErrorMessageTakenFromToStringIfNotPresentInExceptionFromNonFatalCreate() {
+        Throwable error = new Exception() {
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public String toString() {
+                return "expected";
+            }
+        };
+
+        assertNotSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+        assertEquals("expected", ClientExceptionSupport.createNonFatalOrPassthrough(error).getMessage());
+    }
+
+    @Test
+    void testErrorMessageTakenFromToStringIfEmptyInExceptionFromNonFatalCreate() {
+        Throwable error = new Exception("") {
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public String toString() {
+                return "expected";
+            }
+        };
+
+        assertNotSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+        assertEquals("expected", ClientExceptionSupport.createNonFatalOrPassthrough(error).getMessage());
+    }
+
+    @Test
+    void testCauseIsClientIOExceptionExtractedAndPassedThroughFromNonFatalCreate() {
+        Exception error = new RuntimeException("Fatal IO Error", new ClientIOException("real error"));
+
+        assertNotSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+        assertSame(error.getCause(), ClientExceptionSupport.createNonFatalOrPassthrough(error));
+    }
+
+    @Test
+    void testTimeoutExceptionConvertedToClientEquivalent() {
+        TimeoutException error = new TimeoutException("timeout");
+
+        assertNotSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+        assertTrue(ClientExceptionSupport.createNonFatalOrPassthrough(error) instanceof ClientOperationTimedOutException);
+    }
+
+    @Test
+    void testIllegalStateExceptionConvertedToClientEquivalent() {
+        IllegalStateException error = new IllegalStateException("timeout");
+
+        assertNotSame(error, ClientExceptionSupport.createNonFatalOrPassthrough(error));
+        assertTrue(ClientExceptionSupport.createNonFatalOrPassthrough(error) instanceof ClientIllegalStateException);
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientMessageTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientMessageTest.java
new file mode 100644
index 0000000..d80b50f
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientMessageTest.java
@@ -0,0 +1,770 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test the API of {@link ClientMessage}
+ */
+class ClientMessageTest {
+
+    @Test
+    public void testCreateEmpty() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        assertFalse(message.hasProperties());
+        assertFalse(message.hasFooters());
+        assertFalse(message.hasAnnotations());
+    }
+
+    @Test
+    public void testCreateEmptyAdvanced() throws ClientException {
+        AdvancedMessage<String> message = ClientMessage.createAdvancedMessage();
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        assertFalse(message.hasProperties());
+        assertFalse(message.hasFooters());
+        assertFalse(message.hasAnnotations());
+
+        assertNull(message.header());
+        assertNull(message.annotations());
+        assertNull(message.applicationProperties());
+        assertNull(message.footer());
+
+        Header header = new Header();
+        Properties properties = new Properties();
+        MessageAnnotations ma = new MessageAnnotations(new LinkedHashMap<>());
+        ApplicationProperties ap = new ApplicationProperties(new LinkedHashMap<>());
+        Footer ft = new Footer(new LinkedHashMap<>());
+
+        message.header(header);
+        message.properties(properties);
+        message.annotations(ma);
+        message.applicationProperties(ap);
+        message.footer(ft);
+
+        assertSame(header, message.header());
+        assertSame(properties, message.properties());
+        assertSame(ma, message.annotations());
+        assertSame(ap, message.applicationProperties());
+        assertSame(ft, message.footer());
+    }
+
+    @Test
+    public void testCreateWithBody() {
+        ClientMessage<String> message = ClientMessage.create(new AmqpValue<>("test"));
+
+        assertNotNull(message.body());
+        assertNotNull(message.bodySections());
+        assertFalse(message.bodySections().isEmpty());
+
+        assertEquals("test", message.body());
+
+        message.forEachBodySection(value -> {
+            assertEquals(new AmqpValue<>("test"), value);
+        });
+    }
+
+    @Test
+    public void testToAdvancedMessageReturnsSameInstance() throws ClientException {
+        Message<String> message = ClientMessage.create(new AmqpValue<>("test"));
+
+        assertNotNull(message.body());
+
+        AdvancedMessage<String> advanced = message.toAdvancedMessage();
+
+        assertSame(message, advanced);
+
+        assertNotNull(advanced.bodySections());
+        assertFalse(advanced.bodySections().isEmpty());
+
+        assertEquals("test", advanced.body());
+
+        advanced.forEachBodySection(value -> {
+            assertEquals(new AmqpValue<>("test"), value);
+        });
+
+        assertEquals(0, advanced.messageFormat());
+        advanced.messageFormat(17);
+        assertEquals(17, advanced.messageFormat());
+    }
+
+    @Test
+    public void testSetGetHeaderFields() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertEquals(Header.DEFAULT_DURABILITY, message.durable());
+        assertEquals(Header.DEFAULT_FIRST_ACQUIRER, message.firstAcquirer());
+        assertEquals(Header.DEFAULT_DELIVERY_COUNT, message.deliveryCount());
+        assertEquals(Header.DEFAULT_TIME_TO_LIVE, message.timeToLive());
+        assertEquals(Header.DEFAULT_PRIORITY, message.priority());
+
+        message.durable(true);
+        message.firstAcquirer(true);
+        message.deliveryCount(10);
+        message.timeToLive(11);
+        message.priority((byte) 12);
+
+        assertEquals(true, message.durable());
+        assertEquals(true, message.firstAcquirer());
+        assertEquals(10, message.deliveryCount());
+        assertEquals(11, message.timeToLive());
+        assertEquals(12, message.priority());
+    }
+
+    @Test
+    public void testSetGetMessagePropertiesFields() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.messageId());
+        assertNull(message.userId());
+        assertNull(message.to());
+        assertNull(message.subject());
+        assertNull(message.replyTo());
+        assertNull(message.correlationId());
+        assertNull(message.contentType());
+        assertNull(message.contentEncoding());
+        assertEquals(0, message.creationTime());
+        assertEquals(0, message.absoluteExpiryTime());
+        assertNull(message.groupId());
+        assertEquals(0, message.groupSequence());
+        assertNull(message.replyToGroupId());
+
+        message.messageId("message-id");
+        message.userId("user-id".getBytes(StandardCharsets.UTF_8));
+        message.to("to");
+        message.subject("subject");
+        message.replyTo("replyTo");
+        message.correlationId("correlationId");
+        message.contentType("contentType");
+        message.contentEncoding("contentEncoding");
+        message.creationTime(32);
+        message.absoluteExpiryTime(64);
+        message.groupId("groupId");
+        message.groupSequence(128);
+        message.replyToGroupId("replyToGroupId");
+
+        assertEquals("message-id", message.messageId());
+        assertEquals("user-id", new String(message.userId(), StandardCharsets.UTF_8));
+        assertEquals("to", message.to());
+        assertEquals("subject", message.subject());
+        assertEquals("replyTo", message.replyTo());
+        assertEquals("subject", message.subject());
+        assertEquals("correlationId", message.correlationId());
+        assertEquals("contentType", message.contentType());
+        assertEquals("contentEncoding", message.contentEncoding());
+        assertEquals(32, message.creationTime());
+        assertEquals(64, message.absoluteExpiryTime());
+        assertEquals("groupId", message.groupId());
+        assertEquals(128, message.groupSequence());
+        assertEquals("replyToGroupId", message.replyToGroupId());
+    }
+
+    @Test
+    public void testBodySetGet() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        assertNotNull(message.body("test"));
+        assertEquals("test", message.body());
+
+        message.forEachBodySection(value -> {
+            assertEquals(new AmqpValue<>("test"), value);
+        });
+
+        message.clearBodySections();
+
+        assertEquals(0, message.bodySections().size());
+        assertNull(message.body());
+
+        final AtomicInteger count = new AtomicInteger();
+        message.bodySections().forEach(value -> {
+            count.incrementAndGet();
+        });
+
+        assertEquals(0, count.get());
+    }
+
+    @Test
+    public void testForEachMethodsOnEmptyMessage() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertFalse(message.hasProperties());
+        assertFalse(message.hasFooters());
+        assertFalse(message.hasAnnotations());
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        message.forEachBodySection(value -> {
+            fail("Should not invoke any consumers since Message is empty");
+        });
+
+        message.forEachProperty((key, value) -> {
+            fail("Should not invoke any consumers since Message is empty");
+        });
+
+        message.forEachFooter((key, value) -> {
+            fail("Should not invoke any consumers since Message is empty");
+        });
+
+        message.forEachAnnotation((key, value) -> {
+            fail("Should not invoke any consumers since Message is empty");
+        });
+    }
+
+    @Test
+    public void testSetMultipleBodySections() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        List<Section<?>> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new Data(new byte[] { 1 }));
+        expected.add(new Data(new byte[] { 2 }));
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        message.bodySections(expected);
+
+        assertEquals(expected.size(), message.bodySections().size());
+
+        final AtomicInteger count = new AtomicInteger();
+        message.forEachBodySection(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+
+        count.set(0);
+        message.bodySections().forEach(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+
+        message.bodySections(Collections.emptyList());
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        message.bodySections(expected);
+
+        assertEquals(expected.size(), message.bodySections().size());
+
+        message.bodySections(null);
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+    }
+
+    @Test
+    public void testAddMultipleBodySectionsPreservesOriginal() {
+        ClientMessage<byte[]> message = ClientMessage.create();
+
+        List<Data> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 1 }));
+        expected.add(new Data(new byte[] { 2 }));
+        expected.add(new Data(new byte[] { 3 }));
+
+        message.body(new byte[] { 0 });
+
+        assertNotNull(message.body());
+
+        for (Data value : expected) {
+            message.addBodySection(value);
+        }
+
+        assertEquals(expected.size() + 1, message.bodySections().size());
+
+        final AtomicInteger counter = new AtomicInteger();
+        message.bodySections().forEach(section -> {
+            assertTrue(section instanceof Data);
+            final Data dataView = (Data) section;
+            assertEquals(counter.getAndIncrement(), dataView.getBinary().getArray()[0]);
+        });
+    }
+
+    @Test
+    public void testAddMultipleBodySections() {
+        ClientMessage<byte[]> message = ClientMessage.create();
+
+        List<Data> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new Data(new byte[] { 1 }));
+        expected.add(new Data(new byte[] { 2 }));
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        for (Data value : expected) {
+            message.addBodySection(value);
+        }
+
+        assertEquals(expected.size(), message.bodySections().size());
+
+        final AtomicInteger count = new AtomicInteger();
+        message.forEachBodySection(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+
+        count.set(0);
+        message.bodySections().forEach(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+
+        message.clearBodySections();
+
+        assertEquals(0, message.bodySections().size());
+
+        count.set(0);
+        message.bodySections().forEach(value -> {
+            count.incrementAndGet();
+        });
+
+        assertEquals(0, count.get());
+
+        for (Data value : expected) {
+            message.addBodySection(value);
+        }
+
+        assertEquals(expected.size(), message.bodySections().size());
+        message.body(new byte[] { 3 });
+        assertEquals(expected.size(), message.bodySections().size());
+        expected.set(0, new Data(new byte[] { 3 }));
+
+        Iterator<?> expectations = expected.iterator();
+        message.bodySections().forEach(section -> {
+            assertEquals(section, expectations.next());
+        });
+
+        message.body(null);
+        assertNull(message.body());
+        assertEquals(0, message.bodySections().size());
+    }
+
+    @Test
+    public void testMixSingleAndMultipleSectionAccess() {
+        ClientMessage<byte[]> message = ClientMessage.create();
+
+        List<Data> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new Data(new byte[] { 1 }));
+        expected.add(new Data(new byte[] { 2 }));
+
+        assertNull(message.body());
+        assertNotNull(message.bodySections());
+        assertTrue(message.bodySections().isEmpty());
+
+        message.body(expected.get(0).getValue());
+
+        assertEquals(expected.get(0).getValue(), message.body());
+        assertNotNull(message.bodySections());
+        assertFalse(message.bodySections().isEmpty());
+        assertEquals(1, message.bodySections().size());
+
+        message.addBodySection(expected.get(1));
+
+        assertEquals(expected.get(0).getValue(), message.body());
+        assertNotNull(message.bodySections());
+        assertFalse(message.bodySections().isEmpty());
+        assertEquals(2, message.bodySections().size());
+
+        message.addBodySection(expected.get(2));
+
+        assertEquals(expected.get(0).getValue(), message.body());
+        assertNotNull(message.bodySections());
+        assertFalse(message.bodySections().isEmpty());
+        assertEquals(3, message.bodySections().size());
+
+        final AtomicInteger count = new AtomicInteger();
+        message.bodySections().forEach(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+    }
+
+    @Test
+    public void testSetMultipleBodySectionsValidatesDefaultFormat() {
+        ClientMessage<Object> message = ClientMessage.create();
+
+        List<Section<?>> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new AmqpValue<>("test"));
+        expected.add(new AmqpSequence<>(new ArrayList<>()));
+
+        assertThrows(IllegalArgumentException.class, () -> message.bodySections(expected));
+    }
+
+    @Test
+    public void testAddMultipleBodySectionsValidatesDefaultFormat() {
+        ClientMessage<Object> message = ClientMessage.create();
+
+        final List<Section<?>> expected1 = new ArrayList<>();
+        expected1.add(new Data(new byte[] { 0 }));
+        expected1.add(new AmqpValue<>("test"));
+        expected1.add(new AmqpSequence<>(new ArrayList<>()));
+
+        assertThrows(IllegalArgumentException.class, () -> expected1.forEach(section -> message.addBodySection(section)));
+
+        message.clearBodySections();
+
+        final List<Section<?>> expected2 = new ArrayList<>();
+        expected2.add(new AmqpSequence<>(new ArrayList<>()));
+        expected2.add(new Data(new byte[] { 0 }));
+        expected2.add(new AmqpValue<>("test"));
+
+        assertThrows(IllegalArgumentException.class, () -> expected2.forEach(section -> message.addBodySection(section)));
+
+        message.clearBodySections();
+
+        final List<Section<?>> expected3 = new ArrayList<>();
+        expected3.add(new AmqpValue<>("test"));
+        expected3.add(new AmqpSequence<>(new ArrayList<>()));
+        expected3.add(new Data(new byte[] { 0 }));
+
+        assertThrows(IllegalArgumentException.class, () -> expected3.forEach(section -> message.addBodySection(section)));
+    }
+
+    @Test
+    public void testReplaceOriginalWithSetBodySectionDoesNotThrowValidationErrorIfValid() {
+        ClientMessage<Object> message = ClientMessage.create();
+
+        message.body("string");  // AmqpValue
+
+        List<Section<?>> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+
+        assertDoesNotThrow(() -> message.bodySections(expected));
+    }
+
+    @Test
+    public void testReplaceOriginalWithSetBodySectionDoesThrowValidationErrorIfInValid() {
+        ClientMessage<Object> message = ClientMessage.create();
+
+        message.body("string");  // AmqpValue
+
+        List<Section<?>> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new AmqpValue<>("test"));
+        expected.add(new AmqpSequence<>(new ArrayList<>()));
+
+        assertThrows(IllegalArgumentException.class, () -> message.bodySections(expected));
+    }
+
+    @Test
+    public void testAddAdditionalBodySectionsValidatesDefaultFormat() {
+        ClientMessage<Object> message = ClientMessage.create();
+
+        message.body("string");  // AmqpValue
+
+        assertThrows(IllegalArgumentException.class, () -> message.addBodySection(new Data(new byte[] { 0 })));
+    }
+
+    @Test
+    public void testSetMultipleBodySectionsWithNonDefaultMessageFormat() {
+        ClientMessage<Object> message = ClientMessage.create().messageFormat(1);
+
+        List<Section<?>> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new AmqpValue<>("test"));
+        expected.add(new AmqpSequence<>(new ArrayList<>()));
+
+        assertDoesNotThrow(() -> message.bodySections(expected));
+
+        final AtomicInteger count = new AtomicInteger();
+        message.bodySections().forEach(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+    }
+
+    @Test
+    public void testAddMultipleBodySectionsWithNonDefaultMessageFormat() {
+        ClientMessage<Object> message = ClientMessage.create().messageFormat(1);
+
+        List<Section<?>> expected = new ArrayList<>();
+        expected.add(new Data(new byte[] { 0 }));
+        expected.add(new AmqpValue<>("test"));
+        expected.add(new AmqpSequence<>(new ArrayList<>()));
+
+        assertDoesNotThrow(() -> message.bodySections(expected));
+
+        final AtomicInteger count = new AtomicInteger();
+        message.bodySections().forEach(value -> {
+            assertEquals(expected.get(count.get()), value);
+            count.incrementAndGet();
+        });
+
+        assertEquals(expected.size(), count.get());
+    }
+
+    @Test
+    public void testMessageAnnotation() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        final Map<String, String> expectations = new HashMap<>();
+        expectations.put("test1", "1");
+        expectations.put("test2", "2");
+
+        assertFalse(message.hasAnnotations());
+        assertFalse(message.hasAnnotation("test1"));
+
+        assertNotNull(message.annotation("test1", "1"));
+        assertNotNull(message.annotation("test1"));
+
+        assertTrue(message.hasAnnotations());
+        assertTrue(message.hasAnnotation("test1"));
+
+        assertNotNull(message.annotation("test2", "2"));
+        assertNotNull(message.annotation("test2"));
+
+        final AtomicInteger count = new AtomicInteger();
+
+        message.forEachAnnotation((k, v) -> {
+            assertTrue(expectations.containsKey(k));
+            assertEquals(v, expectations.get(k));
+            count.incrementAndGet();
+        });
+
+        assertEquals(expectations.size(), count.get());
+
+        assertEquals("1", message.removeAnnotation("test1"));
+        assertEquals("2", message.removeAnnotation("test2"));
+        assertNull(message.removeAnnotation("test1"));
+        assertNull(message.removeAnnotation("test2"));
+        assertNull(message.removeAnnotation("test3"));
+        assertFalse(message.hasAnnotations());
+        assertFalse(message.hasAnnotation("test1"));
+        assertFalse(message.hasAnnotation("test2"));
+
+        message.forEachAnnotation((k, v) -> {
+            fail("Should not be any remaining Message Annotations");
+        });
+    }
+
+    @Test
+    public void testApplicationProperty() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        final Map<String, String> expectations = new HashMap<>();
+        expectations.put("test1", "1");
+        expectations.put("test2", "2");
+
+        assertFalse(message.hasProperties());
+        assertFalse(message.hasProperty("test1"));
+
+        assertNotNull(message.property("test1", "1"));
+        assertNotNull(message.property("test1"));
+
+        assertTrue(message.hasProperties());
+        assertTrue(message.hasProperty("test1"));
+
+        assertNotNull(message.property("test2", "2"));
+        assertNotNull(message.property("test2"));
+
+        final AtomicInteger count = new AtomicInteger();
+
+        message.forEachProperty((k, v) -> {
+            assertTrue(expectations.containsKey(k));
+            assertEquals(v, expectations.get(k));
+            count.incrementAndGet();
+        });
+
+        assertEquals(expectations.size(), count.get());
+
+        assertEquals("1", message.removeProperty("test1"));
+        assertEquals("2", message.removeProperty("test2"));
+        assertNull(message.removeProperty("test1"));
+        assertNull(message.removeProperty("test2"));
+        assertNull(message.removeProperty("test3"));
+        assertFalse(message.hasProperties());
+        assertFalse(message.hasProperty("test1"));
+        assertFalse(message.hasProperty("test2"));
+
+        message.forEachProperty((k, v) -> {
+            fail("Should not be any remaining Application Properties");
+        });
+    }
+
+    @Test
+    public void testFooter() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        final Map<String, String> expectations = new HashMap<>();
+        expectations.put("test1", "1");
+        expectations.put("test2", "2");
+
+        assertFalse(message.hasFooters());
+        assertFalse(message.hasFooter("test1"));
+
+        assertNotNull(message.footer("test1", "1"));
+        assertNotNull(message.footer("test1"));
+
+        assertTrue(message.hasFooters());
+        assertTrue(message.hasFooter("test1"));
+
+        assertNotNull(message.footer("test2", "2"));
+        assertNotNull(message.footer("test2"));
+
+        final AtomicInteger count = new AtomicInteger();
+
+        message.forEachFooter((k, v) -> {
+            assertTrue(expectations.containsKey(k));
+            assertEquals(v, expectations.get(k));
+            count.incrementAndGet();
+        });
+
+        assertEquals(expectations.size(), count.get());
+
+        assertEquals("1", message.removeFooter("test1"));
+        assertEquals("2", message.removeFooter("test2"));
+        assertNull(message.removeFooter("test1"));
+        assertNull(message.removeFooter("test2"));
+        assertNull(message.removeFooter("test3"));
+        assertFalse(message.hasFooters());
+        assertFalse(message.hasFooter("test1"));
+        assertFalse(message.hasFooter("test2"));
+
+        message.forEachFooter((k, v) -> {
+            fail("Should not be any remaining footers");
+        });
+    }
+
+    @Test
+    public void testGetUserIdHandlesNullPropertiesOrNullUserIDInProperties() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.properties());
+        assertNull(message.userId());
+
+        message.properties(new Properties());
+
+        assertNull(message.userId());
+    }
+
+    @Test
+    public void testApplicationPropertiesAccessorHandlerNullMapOrEmptyMap() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.applicationProperties());
+        assertNull(message.property("test"));
+        assertFalse(message.hasProperty("test"));
+        assertFalse(message.hasProperties());
+
+        message.applicationProperties(new ApplicationProperties(null));
+
+        assertNotNull(message.applicationProperties());
+        assertNull(message.property("test"));
+        assertFalse(message.hasProperty("test"));
+        assertFalse(message.hasProperties());
+    }
+
+    @Test
+    public void testFooterAccessorHandlerNullMapOrEmptyMap() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.footer());
+        assertNull(message.footer("test"));
+        assertFalse(message.hasFooter("test"));
+        assertFalse(message.hasFooters());
+
+        message.footer(new Footer(null));
+
+        assertNotNull(message.footer());
+        assertNull(message.footer("test"));
+        assertFalse(message.hasFooter("test"));
+        assertFalse(message.hasFooters());
+    }
+
+    @Test
+    public void testMessageAnnotationsAccessorHandlerNullMapOrEmptyMap() {
+        ClientMessage<String> message = ClientMessage.create();
+
+        assertNull(message.annotations());
+        assertNull(message.annotation("test"));
+        assertFalse(message.hasAnnotation("test"));
+        assertFalse(message.hasAnnotations());
+
+        message.annotations(new MessageAnnotations(null));
+
+        assertNotNull(message.annotations());
+        assertNull(message.annotation("test"));
+        assertFalse(message.hasAnnotation("test"));
+        assertFalse(message.hasAnnotations());
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientTest.java
new file mode 100644
index 0000000..d6dc949
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ClientTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.ClientOptions;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test the Client API implementation
+ */
+@Timeout(20)
+public class ClientTest extends ImperativeClientTestCase {
+
+    /**
+     * Tests that when using the ClientOptions you need to configure a
+     * container id as that is mandatory and the only reason one would
+     * be supplying ClientOptions instances.
+     */
+    @Test
+    public void testCreateWithNoContainerIdFails() {
+        ClientOptions options = new ClientOptions();
+        assertNull(options.id());
+
+        try {
+            Client.create(options);
+            fail("Should enforce user supplied container Id");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testCreateWithContainerId() {
+        final String id = "test-id";
+
+        ClientOptions options = new ClientOptions();
+        options.id(id);
+        assertNotNull(options.id());
+
+        Client client = Client.create(options);
+        assertNotNull(client.containerId());
+        assertEquals(id, client.containerId());
+    }
+
+    @Test
+    public void testCoseClientAndConnectShouldFail() throws ClientException {
+        Client client = Client.create();
+        assertTrue(client.closeAsync().isDone());
+
+        try {
+            client.connect("localhost");
+            fail("Should enforce no new connections on Client close");
+        } catch (ClientIllegalStateException closed) {
+            // Expected
+        }
+
+        try {
+            client.connect("localhost", new ConnectionOptions());
+            fail("Should enforce no new connections on Client close");
+        } catch (ClientIllegalStateException closed) {
+            // Expected
+        }
+
+        try {
+            client.connect("localhost", 5672);
+            fail("Should enforce no new connections on Client close");
+        } catch (ClientIllegalStateException closed) {
+            // Expected
+        }
+
+        try {
+            client.connect("localhost", 5672, new ConnectionOptions());
+            fail("Should enforce no new connections on Client close");
+        } catch (ClientIllegalStateException closed) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testCloseAllConnectionWhenNonCreatedDoesNotBlock() throws Exception {
+        Client.create().close();
+    }
+
+    @Test
+    public void testCloseAllConnectionAndWait() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer secondPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.start();
+
+            secondPeer.expectSASLAnonymousConnect();
+            secondPeer.expectOpen().respond();
+            secondPeer.start();
+
+            final URI firstURI = firstPeer.getServerURI();
+            final URI secondURI = secondPeer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection1 = container.connect(firstURI.getHost(), firstURI.getPort());
+            Connection connection2 = container.connect(secondURI.getHost(), secondURI.getPort());
+
+            connection1.openFuture().get();
+            connection2.openFuture().get();
+
+            firstPeer.waitForScriptToComplete();
+            secondPeer.waitForScriptToComplete();
+
+            firstPeer.expectClose().respond().afterDelay(10);
+            secondPeer.expectClose().respond().afterDelay(11);
+
+            container.closeAsync().get(5, TimeUnit.SECONDS);
+
+            firstPeer.waitForScriptToComplete();
+            secondPeer.waitForScriptToComplete();
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ConnectionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ConnectionTest.java
new file mode 100644
index 0000000..f8f8228
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ConnectionTest.java
@@ -0,0 +1,1547 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.ClientOptions;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRedirectedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.SourceMatcher;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test for the Connection class
+ */
+@Timeout(20)
+public class ConnectionTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ConnectionTest.class);
+
+    @Test
+    public void testConnectFailsDueToServerStopped() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            peer.close();
+
+            Client container = Client.create();
+
+            try {
+                Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+                connection.openFuture().get();
+                fail("Should fail to connect");
+            } catch (ExecutionException ex) {
+                LOG.info("Connection create failed due to: ", ex);
+                assertTrue(ex.getCause() instanceof ClientException);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateTwoDistinctConnectionsFromSingleClientInstance() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer(testServerOptions());
+             ProtonTestServer secondPeer = new ProtonTestServer(testServerOptions())) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectClose().respond();
+            firstPeer.start();
+
+            secondPeer.expectSASLAnonymousConnect();
+            secondPeer.expectOpen().respond();
+            secondPeer.expectClose().respond();
+            secondPeer.start();
+
+            final URI firstURI = firstPeer.getServerURI();
+            final URI secondURI = secondPeer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection1 = container.connect(firstURI.getHost(), firstURI.getPort(), connectionOptions());
+            Connection connection2 = container.connect(secondURI.getHost(), secondURI.getPort(), connectionOptions());
+
+            connection1.openFuture().get();
+            connection2.openFuture().get();
+
+            connection1.closeAsync().get();
+            connection2.closeAsync().get();
+
+            firstPeer.waitForScriptToComplete();
+            secondPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testCreateConnectionToNonSaslPeer() throws Exception {
+        doConnectionWithUnexpectedHeaderTestImpl(AMQPHeader.getAMQPHeader().toArray());
+    }
+
+    @Test
+    public void testCreateConnectionToNonAmqpPeer() throws Exception {
+        doConnectionWithUnexpectedHeaderTestImpl(new byte[] { 'N', 'O', 'T', '-', 'A', 'M', 'Q', 'P' });
+    }
+
+    private void doConnectionWithUnexpectedHeaderTestImpl(byte[] responseHeader) throws Exception, IOException {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLHeader().respondWithBytes(responseHeader);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = connectionOptions("guest", "guest");
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+            } catch (ExecutionException ex) {}
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionString() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionSignalsEvent() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            final URI remoteURI = peer.getServerURI();
+            final CountDownLatch connected = new CountDownLatch(1);
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(
+                remoteURI.getHost(), remoteURI.getPort(), connectionOptions().connectedHandler((conn, event) -> connected.countDown()));
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionWithConfiguredContainerId() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withContainerId("container-id-test").respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            ClientOptions options = new ClientOptions().id("container-id-test");
+            Client container = Client.create(options);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionStringWithDefaultTcpPort() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = connectionOptions();
+            options.transportOptions().defaultTcpPort(remoteURI.getPort());
+            Connection connection = container.connect(remoteURI.getHost(), options);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionEstablishedHandlerGetsCalled() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            final CountDownLatch established = new CountDownLatch(1);
+            ConnectionOptions options = connectionOptions();
+
+            options.connectedHandler((connection, location) -> {
+                LOG.info("Connection signaled that it was established");
+                established.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            assertTrue(established.await(10, TimeUnit.SECONDS));
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionFailedHandlerGetsCalled() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final CountDownLatch failed = new CountDownLatch(1);
+            ConnectionOptions options = connectionOptions();
+
+            options.disconnectedHandler((connection, location) -> {
+                LOG.info("Connection signaled that it has failed");
+                failed.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.openSession();
+
+            assertTrue(failed.await(10, TimeUnit.SECONDS));
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionWithCredentialsChoosesSASLPlainIfOffered() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLPlainConnect("user", "pass");
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            final CountDownLatch established = new CountDownLatch(1);
+            ConnectionOptions options = connectionOptions("user", "pass");
+
+            options.connectedHandler((connection, location) -> {
+                LOG.info("Connection signaled that it was established");
+                established.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            assertTrue(established.await(10, TimeUnit.SECONDS));
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionWithSASLDisabledToSASLEnabledHost() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectAMQPHeader().respondWithSASLPHeader();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            final ConnectionOptions options = connectionOptions();
+            options.saslOptions().saslEnabled(false);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+                fail("Should not successfully connect to remote");
+            } catch(ExecutionException ex) {
+                assertTrue(ex.getCause() instanceof ClientConnectionRemotelyClosedException);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionCloseGetsResponseWithErrorDoesNotThrowTimedGet() throws Exception {
+        doTestConnectionCloseGetsResponseWithErrorDoesNotThrow(true);
+    }
+
+    @Test
+    public void testConnectionCloseGetsResponseWithErrorDoesNotThrowUntimedGet() throws Exception {
+        doTestConnectionCloseGetsResponseWithErrorDoesNotThrow(false);
+    }
+
+    protected void doTestConnectionCloseGetsResponseWithErrorDoesNotThrow(boolean tiemout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond().withErrorCondition(ConnectionError.CONNECTION_FORCED.toString(), "Not accepting connections");
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            if (tiemout) {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+                // Should close normally and not throw error as we initiated the close.
+                connection.closeAsync().get(10, TimeUnit.SECONDS);
+            } else {
+                connection.openFuture().get();
+                // Should close normally and not throw error as we initiated the close.
+                connection.closeAsync().get();
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testRemotelyCloseConnectionWithRedirect() throws Exception {
+        final String redirectVhost = "vhost";
+        final String redirectNetworkHost = "localhost";
+        final int redirectPort = 5677;
+        final String redirectScheme = "wss";
+        final String redirectPath = "/websockets";
+
+        // Tell the test peer to close the connection when executing its last handler
+        final Map<String, Object> errorInfo = new HashMap<>();
+        errorInfo.put(ClientConstants.OPEN_HOSTNAME.toString(), redirectVhost);
+        errorInfo.put(ClientConstants.NETWORK_HOST.toString(), redirectNetworkHost);
+        errorInfo.put(ClientConstants.PORT.toString(), redirectPort);
+        errorInfo.put(ClientConstants.SCHEME.toString(), redirectScheme);
+        errorInfo.put(ClientConstants.PATH.toString(), redirectPath);
+
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().reject(ConnectionError.REDIRECT.toString(), "Not accepting connections", errorInfo);
+            peer.expectBegin().optional();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            try {
+                connection.defaultSession().openFuture().get();
+                fail("Should not be able to connect since the connection is redirected.");
+            } catch (Exception ex) {
+                LOG.debug("Received expected exception from session open: {}", ex.getMessage());
+                Throwable cause = ex.getCause();
+                assertTrue(cause instanceof ClientConnectionRedirectedException);
+
+                ClientConnectionRedirectedException connectionRedirect = (ClientConnectionRedirectedException) ex.getCause();
+
+                assertEquals(redirectVhost, connectionRedirect.getHostname());
+                assertEquals(redirectNetworkHost, connectionRedirect.getNetworkHost());
+                assertEquals(redirectPort, connectionRedirect.getPort());
+                assertEquals(redirectScheme, connectionRedirect.getScheme());
+                assertEquals(redirectPath, connectionRedirect.getPath());
+
+                URI redirect = connectionRedirect.getRedirectionURI();
+
+                assertEquals(redirectNetworkHost, redirect.getHost());
+                assertEquals(redirectPort, redirect.getPort());
+                assertEquals(redirectScheme, redirect.getScheme());
+                assertEquals(redirectPath, redirect.getPath());
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionBlockingCloseGetsResponseWithErrorDoesNotThrow() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond().withErrorCondition(ConnectionError.CONNECTION_FORCED.toString(), "Not accepting connections");
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get();
+            // Should close normally and not throw error as we initiated the close.
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionClosedWithErrorToRemoteSync() throws Exception {
+        doTestConnectionClosedWithErrorToRemote(false);
+    }
+
+    @Test
+    public void testConnectionClosedWithErrorToRemoteAsync() throws Exception {
+        doTestConnectionClosedWithErrorToRemote(true);
+    }
+
+    private void doTestConnectionClosedWithErrorToRemote(boolean async) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().withError(ConnectionError.CONNECTION_FORCED.toString(), "Closed").respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get();
+            if (async) {
+                connection.closeAsync(ErrorCondition.create(ConnectionError.CONNECTION_FORCED.toString(), "Closed")).get();
+            } else {
+                connection.close(ErrorCondition.create(ConnectionError.CONNECTION_FORCED.toString(), "Closed"));
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionRemoteClosedAfterOpened() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().reject(ConnectionError.CONNECTION_FORCED.toString(), "Not accepting connections");
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testConnectionRemoteClosedAfterOpenedWithEmptyErrorConditionDescription() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().reject(ConnectionError.CONNECTION_FORCED.toString(), (String) null);
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testConnectionRemoteClosedAfterOpenedWithNoRemoteErrorCondition() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().reject();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testConnectionOpenFutureWaitCancelledOnConnectionDropWithTimeout() throws Exception {
+        doTestConnectionOpenFutureWaitCancelledOnConnectionDrop(true);
+    }
+
+    @Test
+    public void testConnectionOpenFutureWaitCancelledOnConnectionDropNoTimeout() throws Exception {
+        doTestConnectionOpenFutureWaitCancelledOnConnectionDrop(false);
+    }
+
+    protected void doTestConnectionOpenFutureWaitCancelledOnConnectionDrop(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.close();
+
+            try {
+                if (timeout) {
+                    connection.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    connection.openFuture().get();
+                }
+                fail("Should have thrown an execution error due to connection drop");
+            } catch (ExecutionException error) {
+                LOG.info("connection open failed with error: ", error);
+            }
+
+            try {
+                if (timeout) {
+                    connection.closeAsync().get(10, TimeUnit.SECONDS);
+                } else {
+                    connection.closeAsync().get();
+                }
+            } catch (Throwable error) {
+                LOG.info("connection close failed with error: ", error);
+                fail("Close should ignore connect error and complete without error.");
+            }
+
+            peer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testRemotelyCloseConnectionDuringSessionCreation() throws Exception {
+        final String BREAD_CRUMB = "ErrorMessageBreadCrumb";
+
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.remoteClose().withErrorCondition(AmqpError.NOT_ALLOWED.toString(), BREAD_CRUMB).queue();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+
+            try {
+                session.openFuture().get();
+                fail("Open should throw error when waiting for remote open and connection remotely closed.");
+            } catch (ExecutionException error) {
+                LOG.info("Session open failed with error: ", error);
+                assertNotNull(error.getMessage(), "Expected exception to have a message");
+                assertTrue(error.getMessage().contains(BREAD_CRUMB), "Expected breadcrumb to be present in message");
+                assertNotNull(error.getCause(), "Execution error should convery the cause");
+                assertTrue(error.getCause() instanceof ClientConnectionRemotelyClosedException);
+            }
+
+            session.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionOpenTimeoutWhenNoRemoteOpenArrivesTimeout() throws Exception {
+        doTestConnectionOpenTimeoutWhenNoRemoteOpenArrives(true);
+    }
+
+    @Test
+    public void testConnectionOpenTimeoutWhenNoRemoteOpenArrivesNoTimeout() throws Exception {
+        doTestConnectionOpenTimeoutWhenNoRemoteOpenArrives(false);
+    }
+
+    private void doTestConnectionOpenTimeoutWhenNoRemoteOpenArrives(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final ConnectionOptions options = connectionOptions().openTimeout(75);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            try {
+                if (timeout) {
+                    connection.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    connection.openFuture().get();
+                }
+
+                fail("Open should timeout when no open response and complete future with error.");
+            } catch (Throwable error) {
+                LOG.info("connection open failed with error: ", error);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionOpenWaitWithTimeoutCanceledWhenConnectionDrops() throws Exception {
+        doTestConnectionOpenWaitCanceledWhenConnectionDrops(true);
+    }
+
+    @Test
+    public void testConnectionOpenWaitWithNoTimeoutCanceledWhenConnectionDrops() throws Exception {
+        doTestConnectionOpenWaitCanceledWhenConnectionDrops(false);
+    }
+
+    private void doTestConnectionOpenWaitCanceledWhenConnectionDrops(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            try {
+                if (timeout) {
+                    connection.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    connection.openFuture().get();
+                }
+
+                fail("Open should timeout when no open response and complete future with error.");
+            } catch (ExecutionException error) {
+                LOG.info("connection open failed with error: ", error);
+                assertTrue(error.getCause() instanceof ClientIOException);
+            }
+
+            connection.client();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionCloseTimeoutWhenNoRemoteCloseArrivesTimeout() throws Exception {
+        doTestConnectionCloseTimeoutWhenNoRemoteCloseArrives(true);
+    }
+
+    @Test
+    public void testConnectionCloseTimeoutWhenNoRemoteCloseArrivesNoTimeout() throws Exception {
+        doTestConnectionCloseTimeoutWhenNoRemoteCloseArrives(false);
+    }
+
+    private void doTestConnectionCloseTimeoutWhenNoRemoteCloseArrives(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final ConnectionOptions options = connectionOptions().closeTimeout(75);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            // Shouldn't throw from close, nothing to be done anyway.
+            try {
+                if (timeout) {
+                    connection.closeAsync().get(10, TimeUnit.SECONDS);
+                } else {
+                    connection.closeAsync().get();
+                }
+            } catch (Throwable error) {
+                LOG.info("connection close failed with error: ", error);
+                fail("Close should ignore lack of close response and complete without error.");
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionCloseWaitWithTimeoutCompletesAfterRemoteConnectionDrops() throws Exception {
+        doTestConnectionCloseWaitCompletesAfterRemoteConnectionDrops(true);
+    }
+
+    @Test
+    public void testConnectionCloseWaitWithNoTimeoutCompletesAfterRemoteConnectionDrops() throws Exception {
+        doTestConnectionCloseWaitCompletesAfterRemoteConnectionDrops(false);
+    }
+
+    private void doTestConnectionCloseWaitCompletesAfterRemoteConnectionDrops(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            // Shouldn't throw from close, nothing to be done anyway.
+            try {
+                if (timeout) {
+                    connection.closeAsync().get(10, TimeUnit.SECONDS);
+                } else {
+                    connection.closeAsync().get();
+                }
+            } catch (Throwable error) {
+                LOG.info("connection close failed with error: ", error);
+                fail("Close should treat Connection drop as success and complete without error.");
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateDefaultSenderFailsOnConnectionWithoutSupportForAnonymousRelay() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(
+                remoteURI.getHost(), remoteURI.getPort(), connectionOptions()).openFuture().get();
+
+            try {
+                connection.defaultSender();
+                fail("Should not be able to get the default sender when remote does not offer anonymous relay");
+            } catch (ClientUnsupportedOperationException unsupported) {
+                LOG.info("Caught expected error: ", unsupported);
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateDefaultSenderOnConnectionWithSupportForAnonymousRelay() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withDesiredCapabilities(ClientConstants.ANONYMOUS_RELAY.toString())
+                             .respond()
+                             .withOfferedCapabilities(ClientConstants.ANONYMOUS_RELAY.toString());
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Sender defaultSender = connection.defaultSender().openFuture().get(5, TimeUnit.SECONDS);
+            assertNotNull(defaultSender);
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionRecreatesAnonymousRelaySenderAfterRemoteCloseOfSender() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withDesiredCapabilities(ClientConstants.ANONYMOUS_RELAY.toString())
+                             .respond()
+                             .withOfferedCapabilities(ClientConstants.ANONYMOUS_RELAY.toString());
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+            peer.remoteDetach().queue();
+            peer.expectDetach();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Sender defaultSender = connection.defaultSender().openFuture().get(5, TimeUnit.SECONDS);
+            assertNotNull(defaultSender);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+            peer.expectClose().respond();
+
+            defaultSender = connection.defaultSender().openFuture().get(5, TimeUnit.SECONDS);
+            assertNotNull(defaultSender);
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateDynamicReceiver() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource(new SourceMatcher().withDynamic(true).withAddress(nullValue()))
+                               .respond();
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Receiver receiver = connection.openDynamicReceiver();
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertNotNull("Remote should have assigned the address for the dynamic receiver", receiver.address());
+
+            receiver.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionSenderOpenHeldUntilConnectionOpenedAndRelaySupportConfirmed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            Sender sender = connection.defaultSender();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // This should happen after we inject the held open and attach
+            peer.expectAttach().withRole(Role.SENDER.getValue()).withTarget().withAddress(Matchers.nullValue()).and().respond();
+            peer.expectClose().respond();
+
+            // Inject held responses to get the ball rolling again
+            peer.remoteOpen().withOfferedCapabilities("ANONYMOUS-RELAY").now();
+            peer.respondToLastBegin().now();
+
+            try {
+                sender.openFuture().get();
+            } catch (ExecutionException ex) {
+                fail("Open of Sender failed waiting for response: " + ex.getCause());
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionSenderIsSingletion() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond().withOfferedCapabilities("ANONYMOUS-RELAY");
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue()).withTarget().withAddress(Matchers.nullValue()).and().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            Sender sender1 = connection.defaultSender();
+            Sender sender2 = connection.defaultSender();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectClose().respond();
+
+            try {
+                sender1.openFuture().get();
+            } catch (ExecutionException ex) {
+                fail("Open of Sender failed waiting for response: " + ex.getCause());
+            }
+
+            try {
+                sender2.openFuture().get();
+            } catch (ExecutionException ex) {
+                fail("Open of Sender failed waiting for response: " + ex.getCause());
+            }
+
+            assertSame(sender1, sender2);
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionSenderOpenFailsWhenAnonymousRelayNotSupported() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions());
+            Sender sender = connection.defaultSender();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectClose().respond();
+
+            try {
+                sender.openFuture().get();
+                fail("Open of Sender should have failed waiting for response when anonymous relay not supported");
+            } catch (ExecutionException ex) {
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionGetRemotePropertiesWaitsForRemoteBegin() throws Exception {
+        tryReadConnectionRemoteProperties(true);
+    }
+
+    @Test
+    public void testConnectionGetRemotePropertiesFailsAfterOpenTimeout() throws Exception {
+        tryReadConnectionRemoteProperties(false);
+    }
+
+    private void tryReadConnectionRemoteProperties(boolean openResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+
+            ConnectionOptions options = connectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Map<String, Object> expectedProperties = new HashMap<>();
+            expectedProperties.put("TEST", "test-property");
+
+            if (openResponse) {
+                peer.expectClose().respond();
+                peer.remoteOpen().withProperties(expectedProperties).later(10);
+            } else {
+                peer.expectClose();
+            }
+
+            if (openResponse) {
+                assertNotNull(connection.properties(), "Remote should have responded with a remote properties value");
+                assertEquals(expectedProperties, connection.properties());
+            } else {
+                try {
+                    connection.properties();
+                    fail("Should failed to get remote state due to no open response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionGetRemoteOfferedCapabilitiesWaitsForRemoteBegin() throws Exception {
+        tryReadConnectionRemoteOfferedCapabilities(true);
+    }
+
+    @Test
+    public void testConnectionGetRemoteOfferedCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadConnectionRemoteOfferedCapabilities(false);
+    }
+
+    private void tryReadConnectionRemoteOfferedCapabilities(boolean openResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = connectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (openResponse) {
+                peer.expectClose().respond();
+                peer.remoteOpen().withOfferedCapabilities("transactions").later(10);
+            } else {
+                peer.expectClose();
+            }
+
+            if (openResponse) {
+                assertNotNull(connection.offeredCapabilities(), "Remote should have responded with a remote offered Capabilities value");
+                assertEquals(1, connection.offeredCapabilities().length);
+                assertEquals("transactions", connection.offeredCapabilities()[0]);
+            } else {
+                try {
+                    connection.offeredCapabilities();
+                    fail("Should failed to get remote state due to no open response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionGetRemoteDesiredCapabilitiesWaitsForRemoteBegin() throws Exception {
+        tryReadConnectionRemoteDesiredCapabilities(true);
+    }
+
+    @Test
+    public void testConnectionGetRemoteDesiredCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadConnectionRemoteDesiredCapabilities(false);
+    }
+
+    private void tryReadConnectionRemoteDesiredCapabilities(boolean openResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = connectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (openResponse) {
+                peer.expectClose().respond();
+                peer.remoteOpen().withDesiredCapabilities("Error-Free").later(10);
+            } else {
+                peer.expectClose();
+            }
+
+            if (openResponse) {
+                assertNotNull(connection.desiredCapabilities(), "Remote should have responded with a remote desired Capabilities value");
+                assertEquals(1, connection.desiredCapabilities().length);
+                assertEquals("Error-Free", connection.desiredCapabilities()[0]);
+            } else {
+                try {
+                    connection.desiredCapabilities();
+                    fail("Should failed to get remote state due to no open response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCloseWithErrorCondition() throws Exception {
+        final String condition = "amqp:precondition-failed";
+        final String description = "something bad happened.";
+
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().withError(condition, description).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(
+                remoteURI.getHost(), remoteURI.getPort(), connectionOptions()).openFuture().get();
+
+            connection.close(ErrorCondition.create(condition, description, null));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAnonymousSenderOpenHeldUntilConnectionOpenedAndSupportConfirmed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Sender sender = connection.openAnonymousSender();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // This should happen after we inject the held open and attach
+            peer.expectAttach().ofSender().withTarget().withAddress(Matchers.nullValue()).and().respond();
+            peer.expectClose().respond();
+
+            // Inject held responses to get the ball rolling again
+            peer.remoteOpen().withOfferedCapabilities("ANONYMOUS-RELAY").now();
+            peer.respondToLastBegin().now();
+
+            try {
+                sender.openFuture().get();
+            } catch (ExecutionException ex) {
+                fail("Open of Sender failed waiting for response: " + ex.getCause());
+            }
+
+            connection.closeAsync();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendHeldUntilConnectionOpenedAndSupportConfirmed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond().withOfferedCapabilities("ANONYMOUS-RELAY");
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withTarget().withAddress(nullValue()).and().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {0}).respond().withSettled(true).withState().accepted();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            try {
+                Tracker tracker = connection.send(Message.create("Hello World"));
+                assertNotNull(tracker);
+                tracker.awaitAccepted();
+            } catch (ClientException ex) {
+                fail("Open of Sender failed waiting for response: " + ex.getCause());
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionLevelSendFailsWhenAnonymousRelayNotAdvertisedByRemote() throws Exception {
+        doTestConnectionLevelSendFailsWhenAnonymousRelayNotAdvertisedByRemote(false);
+    }
+
+    @Test
+    public void testConnectionLevelSendFailsWhenAnonymousRelayNotAdvertisedByRemoteAfterAlreadyOpened() throws Exception {
+        doTestConnectionLevelSendFailsWhenAnonymousRelayNotAdvertisedByRemote(true);
+    }
+
+    private void doTestConnectionLevelSendFailsWhenAnonymousRelayNotAdvertisedByRemote(boolean openWait) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            // Ensures that the Begin arrives regard of a race on open without anonymous relay support
+            connection.defaultSession();
+
+            if (openWait) {
+                connection.openFuture().get();
+            }
+
+            try {
+                connection.send(Message.create("Hello World"));
+                fail("Open of Sender should fail as remote did not advertise anonymous relay support: ");
+            } catch (ClientUnsupportedOperationException ex) {
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenAnonymousSenderFailsWhenAnonymousRelayNotAdvertisedByRemote() throws Exception {
+        doTestOpenAnonymousSenderFailsWhenAnonymousRelayNotAdvertisedByRemote(false);
+    }
+
+    @Test
+    public void testOpenAnonymousSenderFailsWhenAnonymousRelayNotAdvertisedByRemoteAfterAlreadyOpened() throws Exception {
+        doTestOpenAnonymousSenderFailsWhenAnonymousRelayNotAdvertisedByRemote(true);
+    }
+
+    private void doTestOpenAnonymousSenderFailsWhenAnonymousRelayNotAdvertisedByRemote(boolean openWait) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            // Ensures that the Begin arrives regard of a race on open without anonymous relay support
+            connection.defaultSession();
+
+            if (openWait) {
+                connection.openFuture().get();
+            }
+
+            try {
+                connection.openAnonymousSender().openFuture().get();
+                fail("Open of Sender should fail as remote did not advertise anonymous relay support: ");
+            } catch (ClientUnsupportedOperationException ex) {
+            } catch (ExecutionException ex) {
+                assertTrue(ex.getCause() instanceof ClientUnsupportedOperationException);
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenDurableReceiverFromConnection() throws Exception {
+        final String address = "test-topic";
+        final String subscriptionName = "mySubscriptionName";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver()
+                               .withName(subscriptionName)
+                               .withSource()
+                                   .withAddress(address)
+                                   .withDurable(TerminusDurability.UNSETTLED_STATE)
+                                   .withExpiryPolicy(TerminusExpiryPolicy.NEVER)
+                                   .withDistributionMode("copy")
+                               .and().respond();
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Receiver receiver = connection.openDurableReceiver(address, subscriptionName);
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Disabled("Disabled due to requirement of hard coded port")
+    @Test
+    public void testLocalPortOption() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final int localPort = 5671;
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.transportOptions().localPort(localPort);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            connection.openFuture().get();
+
+            assertEquals(localPort, peer.getConnectionRemotePort());
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    protected ProtonTestServerOptions testServerOptions() {
+        return new ProtonTestServerOptions();
+    }
+
+    protected ConnectionOptions connectionOptions() {
+        return new ConnectionOptions();
+    }
+
+    protected ConnectionOptions connectionOptions(String user, String password) {
+        return new ConnectionOptions().user(user).password(password);
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/MessageSendTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/MessageSendTest.java
new file mode 100644
index 0000000..da51690
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/MessageSendTest.java
@@ -0,0 +1,1293 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.DeliveryMode;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.client.util.ExternalMessage;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.ApplicationPropertiesMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.DeliveryAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.FooterMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.HeaderMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.MessageAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.PropertiesMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpSequenceMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedDataMatcher;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests expectations on sends using the various {@link Message} and {@link AdvancedMessage}
+ * API mechanisms
+ */
+@Timeout(20)
+class MessageSendTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MessageSendTest.class);
+
+    @Test
+    public void testSendMessageWithHeaderValuesPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate all Header values
+            message.durable(true);
+            message.priority((byte) 1);
+            message.timeToLive(65535);
+            message.firstAcquirer(true);
+            message.deliveryCount(2);
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithPropertiesValuesPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            PropertiesMatcher propertiesMatcher = new PropertiesMatcher(true);
+            propertiesMatcher.withMessageId("ID:12345");
+            propertiesMatcher.withUserId("user".getBytes(StandardCharsets.UTF_8));
+            propertiesMatcher.withTo("the-management");
+            propertiesMatcher.withSubject("amqp");
+            propertiesMatcher.withReplyTo("the-minions");
+            propertiesMatcher.withCorrelationId("abc");
+            propertiesMatcher.withContentEncoding("application/json");
+            propertiesMatcher.withContentEncoding("gzip");
+            propertiesMatcher.withAbsoluteExpiryTime(123);
+            propertiesMatcher.withCreationTime(1);
+            propertiesMatcher.withGroupId("disgruntled");
+            propertiesMatcher.withGroupSequence(8192);
+            propertiesMatcher.withReplyToGroupId("/dev/null");
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setPropertiesMatcher(propertiesMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate all Properties values
+            message.messageId("ID:12345");
+            message.userId("user".getBytes(StandardCharsets.UTF_8));
+            message.to("the-management");
+            message.subject("amqp");
+            message.replyTo("the-minions");
+            message.correlationId("abc");
+            message.contentEncoding("application/json");
+            message.contentEncoding("gzip");
+            message.absoluteExpiryTime(123);
+            message.creationTime(1);
+            message.groupId("disgruntled");
+            message.groupSequence(8192);
+            message.replyToGroupId("/dev/null");
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithDeliveryAnnotationsPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            DeliveryAnnotationsMatcher daMatcher = new DeliveryAnnotationsMatcher(true);
+            daMatcher.withEntry("one", Matchers.equalTo(1));
+            daMatcher.withEntry("two", Matchers.equalTo(2));
+            daMatcher.withEntry("three", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setDeliveryAnnotationsMatcher(daMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate delivery annotations
+            Map<String, Object> deliveryAnnotations = new HashMap<>();
+            deliveryAnnotations.put("one", 1);
+            deliveryAnnotations.put("two", 2);
+            deliveryAnnotations.put("three", 3);
+
+            final Tracker tracker = sender.send(message, deliveryAnnotations);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithMessageAnnotationsPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            MessageAnnotationsMatcher maMatcher = new MessageAnnotationsMatcher(true);
+            maMatcher.withEntry("one", Matchers.equalTo(1));
+            maMatcher.withEntry("two", Matchers.equalTo(2));
+            maMatcher.withEntry("three", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageAnnotationsMatcher(maMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate message annotations
+            message.annotation("one", 1);
+            message.annotation("two", 2);
+            message.annotation("three", 3);
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithApplicationPropertiesPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("one", Matchers.equalTo(1));
+            apMatcher.withEntry("two", Matchers.equalTo(2));
+            apMatcher.withEntry("three", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate message application properties
+            message.property("one", 1);
+            message.property("two", 2);
+            message.property("three", 3);
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithFootersPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            FooterMatcher footerMatcher = new FooterMatcher(false);
+            footerMatcher.withEntry("f1", Matchers.equalTo(1));
+            footerMatcher.withEntry("f2", Matchers.equalTo(2));
+            footerMatcher.withEntry("f3", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World", true);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+            payloadMatcher.setFootersMatcher(footerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate message footers
+            message.footer("f1", 1);
+            message.footer("f2", 2);
+            message.footer("f3", 3);
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithMultipleSectionsPopulated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            PropertiesMatcher propertiesMatcher = new PropertiesMatcher(true);
+            propertiesMatcher.withMessageId("ID:12345");
+            propertiesMatcher.withUserId("user".getBytes(StandardCharsets.UTF_8));
+            propertiesMatcher.withTo("the-management");
+            propertiesMatcher.withSubject("amqp");
+            propertiesMatcher.withReplyTo("the-minions");
+            propertiesMatcher.withCorrelationId("abc");
+            propertiesMatcher.withContentEncoding("application/json");
+            propertiesMatcher.withContentEncoding("gzip");
+            propertiesMatcher.withAbsoluteExpiryTime(123);
+            propertiesMatcher.withCreationTime(1);
+            propertiesMatcher.withGroupId("disgruntled");
+            propertiesMatcher.withGroupSequence(8192);
+            propertiesMatcher.withReplyToGroupId("/dev/null");
+            DeliveryAnnotationsMatcher daMatcher = new DeliveryAnnotationsMatcher(true);
+            daMatcher.withEntry("da1", Matchers.equalTo(1));
+            daMatcher.withEntry("da2", Matchers.equalTo(2));
+            daMatcher.withEntry("da3", Matchers.equalTo(3));
+            MessageAnnotationsMatcher maMatcher = new MessageAnnotationsMatcher(true);
+            maMatcher.withEntry("ma1", Matchers.equalTo(1));
+            maMatcher.withEntry("ma2", Matchers.equalTo(2));
+            maMatcher.withEntry("ma3", Matchers.equalTo(3));
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("ap1", Matchers.equalTo(1));
+            apMatcher.withEntry("ap2", Matchers.equalTo(2));
+            apMatcher.withEntry("ap3", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World", true);
+            FooterMatcher footerMatcher = new FooterMatcher(false);
+            footerMatcher.withEntry("f1", Matchers.equalTo(1));
+            footerMatcher.withEntry("f2", Matchers.equalTo(2));
+            footerMatcher.withEntry("f3", Matchers.equalTo(3));
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setDeliveryAnnotationsMatcher(daMatcher);
+            payloadMatcher.setMessageAnnotationsMatcher(maMatcher);
+            payloadMatcher.setPropertiesMatcher(propertiesMatcher);
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+            payloadMatcher.setFootersMatcher(footerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            // Populate delivery annotations
+            Map<String, Object> deliveryAnnotations = new HashMap<>();
+            deliveryAnnotations.put("da1", 1);
+            deliveryAnnotations.put("da2", 2);
+            deliveryAnnotations.put("da3", 3);
+
+            final Message<String> message = Message.create("Hello World");
+
+            // Populate all Header values
+            message.durable(true);
+            message.priority((byte) 1);
+            message.timeToLive(65535);
+            message.firstAcquirer(true);
+            message.deliveryCount(2);
+            // Populate message annotations
+            message.annotation("ma1", 1);
+            message.annotation("ma2", 2);
+            message.annotation("ma3", 3);
+            // Populate all Properties values
+            message.messageId("ID:12345");
+            message.userId("user".getBytes(StandardCharsets.UTF_8));
+            message.to("the-management");
+            message.subject("amqp");
+            message.replyTo("the-minions");
+            message.correlationId("abc");
+            message.contentEncoding("application/json");
+            message.contentEncoding("gzip");
+            message.absoluteExpiryTime(123);
+            message.creationTime(1);
+            message.groupId("disgruntled");
+            message.groupSequence(8192);
+            message.replyToGroupId("/dev/null");
+            // Populate message application properties
+            message.property("ap1", 1);
+            message.property("ap2", 2);
+            message.property("ap3", 3);
+            // Populate message footers
+            message.footer("f1", 1);
+            message.footer("f2", 2);
+            message.footer("f3", 3);
+
+            final Tracker tracker = sender.send(message, deliveryAnnotations);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSemdMessageWithUUIDPayloadArrivesWithAMQPValueBodySetFromEmpty() throws Exception {
+        doTestSemdMessageWithUUIDPayloadArrivesWithAMQPValueBody(true);
+    }
+
+    @Test
+    public void testSemdMessageWithUUIDPayloadArrivesWithAMQPValueBodyPopulateOnCreate() throws Exception {
+        doTestSemdMessageWithUUIDPayloadArrivesWithAMQPValueBody(false);
+    }
+
+    private void doTestSemdMessageWithUUIDPayloadArrivesWithAMQPValueBody(boolean useSetter) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            final UUID payload = UUID.randomUUID();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<UUID> message;
+            if (useSetter) {
+                message = Message.<UUID>create().body(payload);
+            } else {
+                message = Message.create(payload);
+            }
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSemdMessageWithByteArrayPayloadArrivesWithDataSectionSetFromEmpty() throws Exception {
+        doTestSemdMessageWithByteArrayPayloadArrivesWithDataSection(true);
+    }
+
+    @Test
+    public void testSemdMessageWithByteArrayPayloadArrivesWithDataSectionPopulateOnCreate() throws Exception {
+        doTestSemdMessageWithByteArrayPayloadArrivesWithDataSection(false);
+    }
+
+    private void doTestSemdMessageWithByteArrayPayloadArrivesWithDataSection(boolean useSetter) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            final byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            EncodedDataMatcher bodyMatcher = new EncodedDataMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<byte[]> message;
+            if (useSetter) {
+                message = Message.<byte[]>create().body(payload);
+            } else {
+                message = Message.create(payload);
+            }
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSemdMessageWithListPayloadArrivesWithAMQPSequenceBodySetFromEmpty() throws Exception {
+        doTestSemdMessageWithListPayloadArrivesWithAMQPSequenceBody(true);
+    }
+
+    @Test
+    public void testSemdMessageWithListPayloadArrivesWithAMQPSequenceBodyPopulateOnCreate() throws Exception {
+        doTestSemdMessageWithListPayloadArrivesWithAMQPSequenceBody(false);
+    }
+
+    private void doTestSemdMessageWithListPayloadArrivesWithAMQPSequenceBody(boolean useSetter) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            final List<UUID> payload = new ArrayList<>();
+            payload.add(UUID.randomUUID());
+            payload.add(UUID.randomUUID());
+            payload.add(UUID.randomUUID());
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            EncodedAmqpSequenceMatcher bodyMatcher = new EncodedAmqpSequenceMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<List<UUID>> message;
+            if (useSetter) {
+                message = Message.<List<UUID>>create().body(payload);
+            } else {
+                message = Message.create(payload);
+            }
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSemdMessageWithMapPayloadArrivesWithAMQPValueBodySetFromEmpty() throws Exception {
+        doTestSemdMessageWithMapPayloadArrivesWithAMQPValueBody(true);
+    }
+
+    @Test
+    public void testSemdMessageWithMapPayloadArrivesWithAMQPValueBodyPopulateOnCreate() throws Exception {
+        doTestSemdMessageWithMapPayloadArrivesWithAMQPValueBody(false);
+    }
+
+    private void doTestSemdMessageWithMapPayloadArrivesWithAMQPValueBody(boolean useSetter) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            final Map<String, UUID> payload = new HashMap<>();
+            payload.put("1", UUID.randomUUID());
+            payload.put("2", UUID.randomUUID());
+            payload.put("3", UUID.randomUUID());
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<Map<String, UUID>> message;
+            if (useSetter) {
+                message = Message.<Map<String, UUID>>create().body(payload);
+            } else {
+                message = Message.create(payload);
+            }
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConvertMessageToAdvancedAndSendAMQPHeader() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(false);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create();
+            final AdvancedMessage<String> advanced = message.toAdvancedMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            advanced.header(header);
+
+            final Tracker tracker = sender.send(advanced);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendOfExternalMessageWithoutAdvancedConverstionSupport() throws Exception {
+        doTestSendOfExternalMessage(false, false);
+    }
+
+    @Test
+    public void testSendOfExternalMessageWithAdvancedConverstionSupport() throws Exception {
+        doTestSendOfExternalMessage(true, false);
+    }
+
+    @Test
+    public void testTrySendOfExternalMessageWithoutAdvancedConverstionSupport() throws Exception {
+        doTestSendOfExternalMessage(false, true);
+    }
+
+    @Test
+    public void testTrySendOfExternalMessageWithAdvancedConverstionSupport() throws Exception {
+        doTestSendOfExternalMessage(true, true);
+    }
+
+    private void doTestSendOfExternalMessage(boolean allowAdvancedConversion, boolean trySend) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            PropertiesMatcher propertiesMatcher = new PropertiesMatcher(true);
+            propertiesMatcher.withMessageId("ID:12345");
+            propertiesMatcher.withUserId("user".getBytes(StandardCharsets.UTF_8));
+            propertiesMatcher.withTo("the-management");
+            propertiesMatcher.withSubject("amqp");
+            propertiesMatcher.withReplyTo("the-minions");
+            propertiesMatcher.withCorrelationId("abc");
+            propertiesMatcher.withContentEncoding("application/json");
+            propertiesMatcher.withContentEncoding("gzip");
+            propertiesMatcher.withAbsoluteExpiryTime(123);
+            propertiesMatcher.withCreationTime(1);
+            propertiesMatcher.withGroupId("disgruntled");
+            propertiesMatcher.withGroupSequence(8192);
+            propertiesMatcher.withReplyToGroupId("/dev/null");
+            DeliveryAnnotationsMatcher daMatcher = new DeliveryAnnotationsMatcher(true);
+            daMatcher.withEntry("da1", Matchers.equalTo(1));
+            daMatcher.withEntry("da2", Matchers.equalTo(2));
+            daMatcher.withEntry("da3", Matchers.equalTo(3));
+            MessageAnnotationsMatcher maMatcher = new MessageAnnotationsMatcher(true);
+            maMatcher.withEntry("ma1", Matchers.equalTo(1));
+            maMatcher.withEntry("ma2", Matchers.equalTo(2));
+            maMatcher.withEntry("ma3", Matchers.equalTo(3));
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("ap1", Matchers.equalTo(1));
+            apMatcher.withEntry("ap2", Matchers.equalTo(2));
+            apMatcher.withEntry("ap3", Matchers.equalTo(3));
+            FooterMatcher footerMatcher = new FooterMatcher(false);
+            footerMatcher.withEntry("f1", Matchers.equalTo(1));
+            footerMatcher.withEntry("f2", Matchers.equalTo(2));
+            footerMatcher.withEntry("f3", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World", true);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setDeliveryAnnotationsMatcher(daMatcher);
+            payloadMatcher.setMessageAnnotationsMatcher(maMatcher);
+            payloadMatcher.setPropertiesMatcher(propertiesMatcher);
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+            payloadMatcher.setFootersMatcher(footerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            // Populate delivery annotations
+            Map<String, Object> deliveryAnnotations = new HashMap<>();
+            deliveryAnnotations.put("da1", 1);
+            deliveryAnnotations.put("da2", 2);
+            deliveryAnnotations.put("da3", 3);
+
+            final Message<String> message = new ExternalMessage<>(allowAdvancedConversion);
+
+            message.body("Hello World");
+            // Populate all Header values
+            message.durable(true);
+            message.priority((byte) 1);
+            message.timeToLive(65535);
+            message.firstAcquirer(true);
+            message.deliveryCount(2);
+            // Populate message annotations
+            message.annotation("ma1", 1);
+            message.annotation("ma2", 2);
+            message.annotation("ma3", 3);
+            // Populate all Properties values
+            message.messageId("ID:12345");
+            message.userId("user".getBytes(StandardCharsets.UTF_8));
+            message.to("the-management");
+            message.subject("amqp");
+            message.replyTo("the-minions");
+            message.correlationId("abc");
+            message.contentEncoding("application/json");
+            message.contentEncoding("gzip");
+            message.absoluteExpiryTime(123);
+            message.creationTime(1);
+            message.groupId("disgruntled");
+            message.groupSequence(8192);
+            message.replyToGroupId("/dev/null");
+            // Populate message application properties
+            message.property("ap1", 1);
+            message.property("ap2", 2);
+            message.property("ap3", 3);
+            // Populate message footers
+            message.footer("f1", 1);
+            message.footer("f2", 2);
+            message.footer("f3", 3);
+
+            // Check preconditions that should affect the send operation
+            if (allowAdvancedConversion) {
+                assertNotNull(message.toAdvancedMessage());
+            } else {
+                assertThrows(UnsupportedOperationException.class, () -> message.toAdvancedMessage());
+            }
+
+            final Tracker tracker;
+            if (trySend) {
+                tracker = sender.trySend(message, deliveryAnnotations);
+            } else {
+                tracker = sender.send(message, deliveryAnnotations);
+            }
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithMultipleAmqpValueSections() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            // Note: This is a specification violation but could be used by other message formats
+            //       and we don't attempt to enforce at the AdvancedMessage API level what users do.
+            EncodedAmqpValueMatcher bodyMatcher1 = new EncodedAmqpValueMatcher("one", true);
+            EncodedAmqpValueMatcher bodyMatcher2 = new EncodedAmqpValueMatcher("two", true);
+            EncodedAmqpValueMatcher bodyMatcher3 = new EncodedAmqpValueMatcher("three", false);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.addMessageContentMatcher(bodyMatcher1);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher2);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher3);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withMessageFormat(17).withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final AdvancedMessage<String> message = AdvancedMessage.create();
+
+            message.messageFormat(17);
+            message.addBodySection(new AmqpValue<>("one"));
+            message.addBodySection(new AmqpValue<>("two"));
+            message.addBodySection(new AmqpValue<>("three"));
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithMultipleAmqpSequenceSections() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            List<String> list1 = new ArrayList<>();
+            list1.add("1");
+            List<String> list2 = new ArrayList<>();
+            list2.add("21");
+            list2.add("22");
+            List<String> list3 = new ArrayList<>();
+            list3.add("31");
+            list3.add("32");
+            list3.add("33");
+
+            EncodedAmqpSequenceMatcher bodyMatcher1 = new EncodedAmqpSequenceMatcher(list1, true);
+            EncodedAmqpSequenceMatcher bodyMatcher2 = new EncodedAmqpSequenceMatcher(list2, true);
+            EncodedAmqpSequenceMatcher bodyMatcher3 = new EncodedAmqpSequenceMatcher(list3, false);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.addMessageContentMatcher(bodyMatcher1);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher2);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher3);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final AdvancedMessage<List<String>> message = AdvancedMessage.create();
+
+            message.addBodySection(new AmqpSequence<>(list1));
+            message.addBodySection(new AmqpSequence<>(list2));
+            message.addBodySection(new AmqpSequence<>(list3));
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageWithMultipleDataSections() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            byte[] buffer1 = new byte[] { 1 };
+            byte[] buffer2 = new byte[] { 1, 2 };
+            byte[] buffer3 = new byte[] { 1, 2, 3 };
+
+            EncodedDataMatcher bodyMatcher1 = new EncodedDataMatcher(buffer1, true);
+            EncodedDataMatcher bodyMatcher2 = new EncodedDataMatcher(buffer2, true);
+            EncodedDataMatcher bodyMatcher3 = new EncodedDataMatcher(buffer3, false);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.addMessageContentMatcher(bodyMatcher1);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher2);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher3);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final AdvancedMessage<byte[]> message = AdvancedMessage.create();
+
+            message.addBodySection(new Data(buffer1));
+            message.addBodySection(new Data(buffer2));
+            message.addBodySection(new Data(buffer3));
+
+            final Tracker tracker = sender.send(message);
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReceiverTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReceiverTest.java
new file mode 100644
index 0000000..d24bd59
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReceiverTest.java
@@ -0,0 +1,2619 @@
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.DeliveryMode;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.DistributionMode;
+import org.apache.qpid.protonj2.client.DurabilityMode;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.ExpiryPolicy;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientLinkRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Timeout(20)
+public class ReceiverTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ReceiverTest.class);
+
+    @Test
+    public void testCreateReceiverAndClose() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLink(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetach() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLink(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDetachLink(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().withSource().withDistributionMode(nullValue()).and().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertSame(container, receiver.client());
+            assertSame(connection, receiver.connection());
+            assertSame(session, receiver.session());
+
+            if (close) {
+                receiver.closeAsync().get(10, TimeUnit.SECONDS);
+            } else {
+                receiver.detachAsync().get(10, TimeUnit.SECONDS);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverAndCloseSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLinkSync(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetachSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLinkSync(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDetachLinkSync(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                receiver.close();
+            } else {
+                receiver.detach();
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverAndCloseWithErrorSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDeatchWithErrorSync(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetachWithErrorSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDeatchWithErrorSync(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDeatchWithErrorSync(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectDetach().withError("amqp-resource-deleted", "an error message").withClosed(close).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                receiver.close(ErrorCondition.create("amqp-resource-deleted", "an error message", null));
+            } else {
+                receiver.detach(ErrorCondition.create("amqp-resource-deleted", "an error message", null));
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverOpenRejectedByRemote() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().respond().withNullSource();
+            peer.expectFlow();
+            peer.remoteDetach().withErrorCondition(AmqpError.UNAUTHORIZED_ACCESS.toString(), "Cannot read from this address").queue();
+            peer.expectDetach();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Receiver receiver = session.openReceiver("test-queue");
+            try {
+                receiver.openFuture().get();
+                fail("Open of receiver should fail due to remote indicating pending close.");
+            } catch (ExecutionException exe) {
+                assertNotNull(exe.getCause());
+                assertTrue(exe.getCause() instanceof ClientLinkRemotelyClosedException);
+                ClientLinkRemotelyClosedException linkClosed = (ClientLinkRemotelyClosedException) exe.getCause();
+                assertNotNull(linkClosed.getErrorCondition());
+                assertEquals(AmqpError.UNAUTHORIZED_ACCESS.toString(), linkClosed.getErrorCondition().condition());
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Should not result in any close being sent now, already closed.
+            receiver.closeAsync().get();
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenReceiverTimesOutWhenNoAttachResponseReceivedTimeout() throws Exception {
+        doTestOpenReceiverTimesOutWhenNoAttachResponseReceived(true);
+    }
+
+    @Test
+    public void testOpenReceiverTimesOutWhenNoAttachResponseReceivedNoTimeout() throws Exception {
+        doTestOpenReceiverTimesOutWhenNoAttachResponseReceived(false);
+    }
+
+    private void doTestOpenReceiverTimesOutWhenNoAttachResponseReceived(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.expectDetach();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Receiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get(10, TimeUnit.SECONDS);
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().openTimeout(10));
+
+            try {
+                if (timeout) {
+                    receiver.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    receiver.openFuture().get();
+                }
+
+                fail("Should not complete the open future without an error");
+            } catch (ExecutionException exe) {
+                Throwable cause = exe.getCause();
+                assertTrue(cause instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenReceiverWaitWithTimeoutFailsWhenConnectionDrops() throws Exception {
+        doTestOpenReceiverWaitFailsWhenConnectionDrops(true);
+    }
+
+    @Test
+    public void testOpenReceiverWaitWithNoTimeoutFailsWhenConnectionDrops() throws Exception {
+        doTestOpenReceiverWaitFailsWhenConnectionDrops(false);
+    }
+
+    private void doTestOpenReceiverWaitFailsWhenConnectionDrops(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver();
+            peer.expectFlow();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+
+            try {
+                if (timeout) {
+                    receiver.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    receiver.openFuture().get();
+                }
+
+                fail("Should not complete the open future without an error");
+            } catch (ExecutionException exe) {
+                Throwable cause = exe.getCause();
+                assertTrue(cause instanceof ClientIOException);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCloseReceiverTimesOutWhenNoCloseResponseReceivedTimeout() throws Exception {
+        doTestCloseOrDetachReceiverTimesOutWhenNoCloseResponseReceived(true, true);
+    }
+
+    @Test
+    public void testCloseReceiverTimesOutWhenNoCloseResponseReceivedNoTimeout() throws Exception {
+        doTestCloseOrDetachReceiverTimesOutWhenNoCloseResponseReceived(true, false);
+    }
+
+    @Test
+    public void testDetachReceiverTimesOutWhenNoCloseResponseReceivedTimeout() throws Exception {
+        doTestCloseOrDetachReceiverTimesOutWhenNoCloseResponseReceived(false, true);
+    }
+
+    @Test
+    public void testDetachReceiverTimesOutWhenNoCloseResponseReceivedNoTimeout() throws Exception {
+        doTestCloseOrDetachReceiverTimesOutWhenNoCloseResponseReceived(false, false);
+    }
+
+    private void doTestCloseOrDetachReceiverTimesOutWhenNoCloseResponseReceived(boolean close, boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.expectDetach();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Receiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.closeTimeout(5);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            try {
+                if (close) {
+                    if (timeout) {
+                        receiver.closeAsync().get(10, TimeUnit.SECONDS);
+                    } else {
+                        receiver.closeAsync().get();
+                    }
+                } else {
+                    if (timeout) {
+                        receiver.detachAsync().get(10, TimeUnit.SECONDS);
+                    } else {
+                        receiver.detachAsync().get();
+                    }
+                }
+
+                fail("Should not complete the close or detach future without an error");
+            } catch (ExecutionException exe) {
+                Throwable cause = exe.getCause();
+                assertTrue(cause instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverDrainAllOutstanding() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(5, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(5, TimeUnit.SECONDS);
+
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(5, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Add some credit, verify not draining
+            int credit = 7;
+            Matcher<Boolean> drainMatcher = anyOf(equalTo(false), nullValue());
+            peer.expectFlow().withDrain(drainMatcher).withLinkCredit(credit).withDeliveryCount(0);
+
+            receiver.addCredit(credit);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Drain all the credit
+            peer.expectFlow().withDrain(true).withLinkCredit(credit).withDeliveryCount(0)
+                             .respond()
+                             .withDrain(true).withLinkCredit(0).withDeliveryCount(credit);
+
+            Future<? extends Receiver> draining = receiver.drain();
+            draining.get(5, TimeUnit.SECONDS);
+
+            // Close things down
+            peer.expectClose().respond();
+            connection.closeAsync().get(5, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainCompletesWhenReceiverHasNoCredit() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(5, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Future<? extends Receiver> draining = receiver.drain();
+            draining.get(5, TimeUnit.SECONDS);
+
+            // Close things down
+            peer.expectClose().respond();
+            connection.closeAsync().get(5, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainAdditionalDrainCallThrowsWhenReceiverStillDraining() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            receiver.drain();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientIllegalStateException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAddCreditFailsWhileDrainPending() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond().withInitialDeliveryCount(20);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Add some credit, verify not draining
+            int credit = 7;
+            Matcher<Boolean> drainMatcher = anyOf(equalTo(false), nullValue());
+            peer.expectFlow().withDrain(drainMatcher).withLinkCredit(credit);
+
+            receiver.addCredit(credit);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Drain all the credit
+            peer.expectFlow().withDrain(true).withLinkCredit(credit).withDeliveryCount(20);
+            peer.expectClose().respond();
+
+            Future<? extends Receiver> draining = receiver.drain();
+            assertFalse(draining.isDone());
+
+            try {
+                receiver.addCredit(1);
+                fail("Should not allow add credit when drain is pending");
+            } catch (ClientIllegalStateException ise) {
+                // Expected
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAddCreditFailsWhenCreditWindowEnabled() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(10));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectClose().respond();
+
+            try {
+                receiver.addCredit(1);
+                fail("Should not allow add credit when credit window configured");
+            } catch (ClientIllegalStateException ise) {
+                // Expected
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateDynamicReceiver() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource().withDynamic(true).withAddress((String) null)
+                               .and().respond()
+                               .withSource().withDynamic(true).withAddress("test-dynamic-node");
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Receiver receiver = session.openDynamicReceiver();
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertNotNull("Remote should have assigned the address for the dynamic receiver", receiver.address());
+            assertEquals("test-dynamic-node", receiver.address());
+
+            receiver.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateDynamicReceiverWthNodeProperties() throws Exception {
+        final Map<String, Object> nodeProperties = new HashMap<>();
+        nodeProperties.put("test-property-1", "one");
+        nodeProperties.put("test-property-2", "two");
+        nodeProperties.put("test-property-3", "three");
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource()
+                                   .withDynamic(true)
+                                   .withAddress((String) null)
+                                   .withDynamicNodeProperties(nodeProperties)
+                               .and().respond()
+                               .withSource()
+                                   .withDynamic(true)
+                                   .withAddress("test-dynamic-node")
+                                   .withDynamicNodeProperties(nodeProperties);
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openDynamicReceiver(nodeProperties);
+
+            assertNotNull("Remote should have assigned the address for the dynamic receiver", receiver.address());
+            assertEquals("test-dynamic-node", receiver.address());
+
+            receiver.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateDynamicReceiverWithNoCreditWindow() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource().withDynamic(true).withAddress((String) null)
+                               .and().respond()
+                               .withSource().withDynamic(true).withAddress("test-dynamic-node");
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions receiverOptions = new ReceiverOptions().creditWindow(0);
+            Receiver receiver = session.openDynamicReceiver(receiverOptions).openFuture().get();
+
+            // Perform another round trip operation to ensure we see that no flow frame was
+            // sent by the receiver
+            session.openSender("test");
+
+            assertNotNull("Remote should have assigned the address for the dynamic receiver", receiver.address());
+            assertEquals("test-dynamic-node", receiver.address());
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDynamicReceiverAddressWaitsForRemoteAttach() throws Exception {
+        tryReadDynamicReceiverAddress(true);
+    }
+
+    @Test
+    public void testDynamicReceiverAddressFailsAfterOpenTimeout() throws Exception {
+        tryReadDynamicReceiverAddress(false);
+    }
+
+    private void tryReadDynamicReceiverAddress(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource().withDynamic(true).withAddress((String) null);
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Receiver receiver = session.openDynamicReceiver();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withSource().withAddress("test-dynamic-node").and().later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull("Remote should have assigned the address for the dynamic receiver", receiver.address());
+                assertEquals("test-dynamic-node", receiver.address());
+            } else {
+                try {
+                    receiver.address();
+                    fail("Should failed to get address due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from address call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverWithQoSOfAtMostOnce() throws Exception {
+        doTestCreateReceiverWithConfiguredQoS(DeliveryMode.AT_MOST_ONCE);
+    }
+
+    @Test
+    public void testCreateReceiverWithQoSOfAtLeastOnce() throws Exception {
+        doTestCreateReceiverWithConfiguredQoS(DeliveryMode.AT_LEAST_ONCE);
+    }
+
+    private void doTestCreateReceiverWithConfiguredQoS(DeliveryMode qos) throws Exception {
+        byte sndMode = qos == DeliveryMode.AT_MOST_ONCE ? SenderSettleMode.SETTLED.byteValue() : SenderSettleMode.UNSETTLED.byteValue();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSndSettleMode(sndMode)
+                               .withRcvSettleMode(ReceiverSettleMode.FIRST.byteValue())
+                               .respond()
+                               .withSndSettleMode(sndMode)
+                               .withRcvSettleMode(ReceiverSettleMode.FIRST.byteValue());
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            ReceiverOptions options = new ReceiverOptions().deliveryMode(qos);
+            Receiver receiver = session.openReceiver("test-qos", options);
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertEquals("test-qos", receiver.address());
+
+            receiver.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetSourceWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverSource(true);
+    }
+
+    @Test
+    public void testReceiverGetSourceFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverSource(false);
+    }
+
+    private void tryReadReceiverSource(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Receiver receiver = session.openReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.source(), "Remote should have responded with a Source value");
+                assertEquals("test-receiver", receiver.source().address());
+            } else {
+                try {
+                    receiver.source();
+                    fail("Should failed to get remote source due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetTargetWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverTarget(true);
+    }
+
+    @Test
+    public void testReceiverGetTargetFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverTarget(false);
+    }
+
+    private void tryReadReceiverTarget(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Receiver receiver = session.openReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.target(), "Remote should have responded with a Target value");
+            } else {
+                try {
+                    receiver.target();
+                    fail("Should failed to get remote source due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetRemotePropertiesWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverRemoteProperties(true);
+    }
+
+    @Test
+    public void testReceiverGetRemotePropertiesFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverRemoteProperties(false);
+    }
+
+    private void tryReadReceiverRemoteProperties(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            ReceiverOptions options = new ReceiverOptions().openTimeout(100);
+            Receiver receiver = session.openReceiver("test-receiver", options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Map<String, Object> expectedProperties = new HashMap<>();
+            expectedProperties.put("TEST", "test-property");
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withPropertiesMap(expectedProperties).later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.properties(), "Remote should have responded with a remote properties value");
+                assertEquals(expectedProperties, receiver.properties());
+            } else {
+                try {
+                    receiver.properties();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetRemoteOfferedCapabilitiesWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverRemoteOfferedCapabilities(true);
+    }
+
+    @Test
+    public void testReceiverGetRemoteOfferedCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverRemoteOfferedCapabilities(false);
+    }
+
+    private void tryReadReceiverRemoteOfferedCapabilities(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Receiver receiver = session.openReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withOfferedCapabilities("QUEUE").later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.offeredCapabilities(), "Remote should have responded with a remote offered Capabilities value");
+                assertEquals(1, receiver.offeredCapabilities().length);
+                assertEquals("QUEUE", receiver.offeredCapabilities()[0]);
+            } else {
+                try {
+                    receiver.offeredCapabilities();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetRemoteDesiredCapabilitiesWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverRemoteDesiredCapabilities(true);
+    }
+
+    @Test
+    public void testReceiverGetRemoteDesiredCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverRemoteDesiredCapabilities(false);
+    }
+
+    private void tryReadReceiverRemoteDesiredCapabilities(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Receiver receiver = session.openReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withDesiredCapabilities("Error-Free").later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.desiredCapabilities(), "Remote should have responded with a remote desired Capabilities value");
+                assertEquals(1, receiver.desiredCapabilities().length);
+                assertEquals("Error-Free", receiver.desiredCapabilities()[0]);
+            } else {
+                try {
+                    receiver.desiredCapabilities();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBlockingReceiveCancelledWhenReceiverClosed() throws Exception {
+        doTtestBlockingReceiveCancelledWhenReceiverClosedOrDetached(true);
+    }
+
+    @Test
+    public void testBlockingReceiveCancelledWhenReceiverDetached() throws Exception {
+        doTtestBlockingReceiveCancelledWhenReceiverClosedOrDetached(false);
+    }
+
+    public void doTtestBlockingReceiveCancelledWhenReceiverClosedOrDetached(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions options = new ReceiverOptions().creditWindow(0);
+            Receiver receiver = session.openReceiver("test-queue", options);
+            receiver.openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withLinkCredit(10);
+            peer.execute(() -> {
+                if (close) {
+                    receiver.closeAsync();
+                } else {
+                    receiver.detachAsync();
+                }
+            }).queue();
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectClose().respond();
+
+            receiver.addCredit(10);
+
+            try {
+                assertNull(receiver.receive());
+            } catch (ClientException ise) {
+                // Can happen if receiver closed before the receive call gets executed.
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBlockingReceiveCancelledWhenReceiverRemotelyClosed() throws Exception {
+        doTtestBlockingReceiveCancelledWhenReceiverClosedOrDetached(true);
+    }
+
+    @Test
+    public void testBlockingReceiveCancelledWhenReceiverRemotelyDetached() throws Exception {
+        doTtestBlockingReceiveCancelledWhenReceiverClosedOrDetached(false);
+    }
+
+    public void doTtestBlockingReceiveCancelledWhenReceiverRemotelyClosedOrDetached(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteDetach().withClosed(close)
+                               .withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Address was manually deleted")
+                               .afterDelay(10).queue();
+            peer.expectDetach().withClosed(close);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get();
+
+            try {
+                assertNull(receiver.receive());
+            } catch (IllegalStateException ise) {
+                // Can happen if receiver closed before the receive call gets executed.
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCloseReceiverWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(true);
+    }
+
+    @Test
+    public void testDetachReceiverWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(false);
+    }
+
+    public void doTestCloseOrDetachWithErrorCondition(boolean close) throws Exception {
+        final String condition = "amqp:link:detach-forced";
+        final String description = "something bad happened.";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.expectDetach().withClosed(close).withError(condition, description).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            final Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get();
+
+            if (close) {
+                receiver.closeAsync(ErrorCondition.create(condition, description, null));
+            } else {
+                receiver.detachAsync(ErrorCondition.create(condition, description, null));
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenReceiverWithLinCapabilities() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource().withCapabilities("queue").and()
+                               .respond();
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Receiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get(10, TimeUnit.SECONDS);
+            ReceiverOptions receiverOptions = new ReceiverOptions();
+            receiverOptions.sourceOptions().capabilities("queue");
+            Receiver receiver = session.openReceiver("test-queue", receiverOptions);
+
+            receiver.openFuture().get();
+            receiver.close();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveMessageInSplitTransferFrames() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            final Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get();
+
+            final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+            final byte[] slice1 = Arrays.copyOfRange(payload, 0, 2);
+            final byte[] slice2 = Arrays.copyOfRange(payload, 2, 4);
+            final byte[] slice3 = Arrays.copyOfRange(payload, 4, payload.length);
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(slice1).now();
+
+            assertNull(receiver.tryReceive());
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(slice2).now();
+
+            assertNull(receiver.tryReceive());
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(slice3).now();
+
+            peer.expectDisposition().withSettled(true).withState().accepted();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Delivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            Message<?> received = delivery.message();
+            assertNotNull(received);
+            assertTrue(received.body() instanceof String);
+            String value = (String) received.body();
+            assertEquals("Hello World", value);
+
+            delivery.accept();
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverHandlesAbortedSplitFrameTransfer() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            final Receiver receiver = session.openReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            assertNull(receiver.receive(10, TimeUnit.MILLISECONDS));
+
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.remoteTransfer().withHandle(0)
+                                 .withMore(false)
+                                 .withAborted(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).now();
+
+            assertNull(receiver.receive(15, TimeUnit.MILLISECONDS));
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverAddCreditOnAbortedTransferWhenNeeded() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions options = new ReceiverOptions();
+            options.creditWindow(1);
+            final Receiver receiver = session.openReceiver("test-queue", options);
+            receiver.openFuture().get();
+
+            final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).now();
+
+            assertNull(receiver.tryReceive());
+
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(1)
+                                 .withMessageFormat(0)
+                                 .withMore(false)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withSettled(true).withState().accepted();
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            // Send final aborted transfer to complete first transfer and allow next to commence.
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withAborted(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).now();
+
+            Delivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            Message<?> received = delivery.message();
+            assertNotNull(received);
+            assertTrue(received.body() instanceof String);
+            String value = (String) received.body();
+            assertEquals("Hello World", value);
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverHandlesAbortedSplitFrameTransferAndReplenishesCredit() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(1);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions options = new ReceiverOptions();
+            options.creditWindow(1);
+            final Receiver receiver = session.openReceiver("test-queue", options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            assertNull(receiver.receive(10, TimeUnit.MILLISECONDS));
+
+            // Credit window is one and next transfer signals aborted so receiver should
+            // top-up the credit window to allow more transfers to arrive.
+            peer.expectFlow().withLinkCredit(1);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            // Abort the delivery which should result in a credit top-up.
+            peer.remoteTransfer().withHandle(0)
+                                 .withMore(false)
+                                 .withAborted(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).now();
+
+            assertNull(receiver.receive(15, TimeUnit.MILLISECONDS));
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveCallFailsWhenReceiverPreviouslyClosed() throws Exception {
+        doTestReceiveCallFailsWhenReceiverDetachedOrClosed(true);
+    }
+
+    @Test
+    public void testReceiveCallFailsWhenReceiverPreviouslyDetached() throws Exception {
+        doTestReceiveCallFailsWhenReceiverDetachedOrClosed(false);
+    }
+
+    private void doTestReceiveCallFailsWhenReceiverDetachedOrClosed(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().withClosed(close).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            if (close) {
+                receiver.closeAsync();
+            } else {
+                receiver.detachAsync();
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            try {
+                receiver.receive();
+                fail("Receive call should fail when link closed or detached.");
+            } catch (ClientIllegalStateException cliEx) {
+                LOG.debug("Receiver threw error on receive call", cliEx);
+            }
+
+            peer.expectClose().respond();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveBlockedForMessageFailsWhenConnectionRemotelyClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteClose().withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Connection was deleted").afterDelay(25).queue();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Receiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get();
+
+            try {
+                receiver.receive();
+                fail("Receive should have failed when Connection remotely closed.");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTimedReceiveBlockedForMessageFailsWhenConnectionRemotelyClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteClose().withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Connection was deleted").afterDelay(25).queue();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Receiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+            receiver.openFuture().get();
+
+            try {
+                receiver.receive(10, TimeUnit.SECONDS);
+                fail("Receive should have failed when Connection remotely closed.");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected send to throw indicating that the remote closed the connection
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveTimedCallFailsWhenReceiverClosed() throws Exception {
+        doTestReceiveTimedCallFailsWhenReceiverDetachedOrClosed(true);
+    }
+
+    @Test
+    public void testReceiveTimedCallFailsWhenReceiverDetached() throws Exception {
+        doTestReceiveTimedCallFailsWhenReceiverDetachedOrClosed(false);
+    }
+
+    private void doTestReceiveTimedCallFailsWhenReceiverDetachedOrClosed(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().withClosed(close).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            if (close) {
+                receiver.closeAsync();
+            } else {
+                receiver.detachAsync();
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            try {
+                receiver.receive(60, TimeUnit.SECONDS);
+                fail("Receive call should fail when link closed or detached.");
+            } catch (ClientIllegalStateException cliEx) {
+                LOG.debug("Receiver threw error on receive call", cliEx);
+            }
+
+            peer.expectClose().respond();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenReceiverClosed() throws Exception {
+        doTestDrainFutureSignalsFailureWhenReceiverClosedOrDetached(true);
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenReceiverDetached() throws Exception {
+        doTestDrainFutureSignalsFailureWhenReceiverClosedOrDetached(false);
+    }
+
+    private void doTestDrainFutureSignalsFailureWhenReceiverClosedOrDetached(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDrain(true).withLinkCredit(10);
+            peer.execute(() -> {
+                if (close) {
+                    receiver.closeAsync();
+                } else {
+                    receiver.detachAsync();
+                }
+            }).queue();
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectClose().respond();
+
+            try {
+                receiver.drain().get(10, TimeUnit.SECONDS);
+                fail("Drain call should fail when link closed or detached.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenReceiverRemotelyClosed() throws Exception {
+        doTestDrainFutureSignalsFailureWhenReceiverRemotelyClosedOrDetached(true);
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenReceiverRemotelyDetached() throws Exception {
+        doTestDrainFutureSignalsFailureWhenReceiverRemotelyClosedOrDetached(false);
+    }
+
+    private void doTestDrainFutureSignalsFailureWhenReceiverRemotelyClosedOrDetached(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDrain(true).withLinkCredit(10);
+            peer.remoteDetach().withClosed(close)
+                               .withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Address was manually deleted").queue();
+            peer.expectDetach().withClosed(close);
+            peer.expectClose().respond();
+
+            try {
+                receiver.drain().get(10, TimeUnit.SECONDS);
+                fail("Drain call should fail when link closed or detached.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenSessionRemotelyClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.remoteEnd().withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Session was closed").afterDelay(5).queue();
+            peer.expectEnd();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            try {
+                receiver.drain().get(10, TimeUnit.SECONDS);
+                fail("Drain call should fail when session closed by remote.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenConnectionDrops() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.dropAfterLastHandler();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            try {
+                receiver.drain().get(10, TimeUnit.SECONDS);
+                fail("Drain call should fail when the connection drops.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenDrainTimeoutExceeded() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions receiverOptions = new ReceiverOptions().drainTimeout(15);
+            Receiver receiver = session.openReceiver("test-queue", receiverOptions).openFuture().get();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenSessionDrainTimeoutExceeded() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            SessionOptions sessionOptions = new SessionOptions().drainTimeout(20);
+            Session session = connection.openSession(sessionOptions);
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenConnectionDrainTimeoutExceeded() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().drainTimeout(20);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBlockedReceiveThrowsConnectionRemotelyClosedError() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().withSource().withAddress("test").and().respond();
+            peer.expectFlow();
+            peer.dropAfterLastHandler(25);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test");
+
+            try {
+                receiver.receive();
+                fail("Receive should fail with remotely closed error after remote drops");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDeliveryRefusesRawStreamAfterMessage() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withSettled(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Delivery delivery = receiver.receive(10, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+
+            Message<String> message = delivery.message();
+            assertNotNull(message);
+
+            try {
+                delivery.rawInputStream();
+                fail("Should not be able to use the inputstream once message is requested");
+            } catch (ClientIllegalStateException cliEx) {
+                // Expected
+            }
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDeliveryRefusesRawStreamAfterAnnotations() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withSettled(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Delivery delivery = receiver.receive(5, TimeUnit.SECONDS);
+            assertNotNull(delivery);
+            assertNull(delivery.annotations());
+
+            try {
+                delivery.rawInputStream();
+                fail("Should not be able to use the inputstream once message is requested");
+            } catch (ClientIllegalStateException cliEx) {
+                // Expected
+            }
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDeliveryRefusesMessageDecodeOnceRawInputStreamIsRequested() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withSettled(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Delivery delivery = receiver.receive(10, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+            InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload.length, stream.available());
+            byte[] bytesRead = new byte[payload.length];
+            assertEquals(payload.length, stream.read(bytesRead));
+            assertArrayEquals(payload, bytesRead);
+
+            try {
+                delivery.message();
+                fail("Should not be able to use the message API once raw stream is requested");
+            } catch (ClientIllegalStateException cliEx) {
+                // Expected
+            }
+
+            try {
+                delivery.annotations();
+                fail("Should not be able to use the annotations API once raw stream is requested");
+            } catch (ClientIllegalStateException cliEx) {
+                // Expected
+            }
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveDeliveryWithMultipleDataSections() throws Exception {
+        final Data section1 = new Data(new byte[] { 0, 1, 2, 3 });
+        final Data section2 = new Data(new byte[] { 0, 1, 2, 3 });
+        final Data section3 = new Data(new byte[] { 0, 1, 2, 3 });
+
+        final byte[] payload = createEncodedMessage(section1, section2, section3);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withSettled(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Delivery delivery = receiver.receive(10, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+
+            AdvancedMessage<?> message = delivery.message().toAdvancedMessage();
+            assertNotNull(message);
+
+            assertEquals(3, message.bodySections().size());
+            List<Section<?>> section = new ArrayList<>(message.bodySections());
+            assertEquals(section1, section.get(0));
+            assertEquals(section2, section.get(1));
+            assertEquals(section3, section.get(2));
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionWindowExpandedAsIncomingFramesArrive() throws Exception {
+        final byte[] payload1 = new byte[255];
+        final byte[] payload2 = new byte[255];
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1024).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withSettled(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withSettled(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(9);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().maxFrameSize(1024);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            SessionOptions sessionOpts = new SessionOptions().incomingCapacity(1024);
+            Session session = connection.openSession(sessionOpts);
+            Receiver receiver = session.openReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Delivery delivery = receiver.receive(10, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+
+            InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload1.length + payload2.length, stream.available());
+            byte[] bytesRead = new byte[payload1.length + payload2.length];
+            assertEquals(payload1.length + payload2.length, stream.read(bytesRead));
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotReadFromStreamDeliveredBeforeConnectionDrop() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.dropAfterLastHandler();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final Receiver receiver = connection.openReceiver("test-queue");
+            final Delivery delivery = receiver.receive();
+
+            peer.waitForScriptToComplete();
+
+            assertNotNull(delivery);
+
+            // Data already read so it will be already available for read.
+            assertNotEquals(-1, delivery.rawInputStream().read());
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverWithDefaultSourceAndTargetOptions() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver()
+                               .withSource().withAddress("test-queue")
+                                            .withDistributionMode(nullValue())
+                                            .withDefaultTimeout()
+                                            .withDurable(TerminusDurability.NONE)
+                                            .withExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH)
+                                            .withDefaultOutcome(new Modified().setDeliveryFailed(true))
+                                            .withCapabilities(nullValue())
+                                            .withFilter(nullValue())
+                                            .withOutcomes("amqp:accepted:list", "amqp:rejected:list", "amqp:released:list", "amqp:modified:list")
+                                            .also()
+                               .withTarget().withAddress(notNullValue())
+                                            .withCapabilities(nullValue())
+                                            .withDurable(nullValue())
+                                            .withExpiryPolicy(nullValue())
+                                            .withDefaultTimeout()
+                                            .withDynamic(anyOf(nullValue(), equalTo(false)))
+                                            .withDynamicNodeProperties(nullValue())
+                               .and().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            receiver.close();
+            session.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverWithUserConfiguredSourceAndTargetOptions() throws Exception {
+        final Map<String, Object> filtersToObject = new HashMap<>();
+        filtersToObject.put("x-opt-filter", "a = b");
+
+        final Map<String, String> filters = new HashMap<>();
+        filters.put("x-opt-filter", "a = b");
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver()
+                               .withSource().withAddress("test-queue")
+                                            .withDistributionMode("copy")
+                                            .withTimeout(128)
+                                            .withDurable(TerminusDurability.UNSETTLED_STATE)
+                                            .withExpiryPolicy(TerminusExpiryPolicy.CONNECTION_CLOSE)
+                                            .withDefaultOutcome(new Released())
+                                            .withCapabilities("QUEUE")
+                                            .withFilter(filtersToObject)
+                                            .withOutcomes("amqp:accepted:list", "amqp:rejected:list")
+                                            .also()
+                               .withTarget().withAddress(notNullValue())
+                                            .withCapabilities("QUEUE")
+                                            .withDurable(TerminusDurability.CONFIGURATION)
+                                            .withExpiryPolicy(TerminusExpiryPolicy.SESSION_END)
+                                            .withTimeout(42)
+                                            .withDynamic(anyOf(nullValue(), equalTo(false)))
+                                            .withDynamicNodeProperties(nullValue())
+                               .and().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions receiverOptions = new ReceiverOptions();
+
+            receiverOptions.sourceOptions().capabilities("QUEUE");
+            receiverOptions.sourceOptions().distributionMode(DistributionMode.COPY);
+            receiverOptions.sourceOptions().timeout(128);
+            receiverOptions.sourceOptions().durabilityMode(DurabilityMode.UNSETTLED_STATE);
+            receiverOptions.sourceOptions().expiryPolicy(ExpiryPolicy.CONNECTION_CLOSE);
+            receiverOptions.sourceOptions().defaultOutcome(DeliveryState.released());
+            receiverOptions.sourceOptions().filters(filters);
+            receiverOptions.sourceOptions().outcomes(DeliveryState.Type.ACCEPTED, DeliveryState.Type.REJECTED);
+
+            receiverOptions.targetOptions().capabilities("QUEUE");
+            receiverOptions.targetOptions().durabilityMode(DurabilityMode.CONFIGURATION);
+            receiverOptions.targetOptions().expiryPolicy(ExpiryPolicy.SESSION_CLOSE);
+            receiverOptions.targetOptions().timeout(42);
+
+            Receiver receiver = session.openReceiver("test-queue", receiverOptions).openFuture().get();
+
+            receiver.close();
+            session.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenDurableReceiver() throws Exception {
+        final String address = "test-topic";
+        final String subscriptionName = "mySubscriptionName";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver()
+                               .withName(subscriptionName)
+                               .withSource()
+                                   .withAddress(address)
+                                   .withDurable(TerminusDurability.UNSETTLED_STATE)
+                                   .withExpiryPolicy(TerminusExpiryPolicy.NEVER)
+                                   .withDistributionMode("copy")
+                               .and().respond();
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openDurableReceiver(address, subscriptionName);
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectReceiverTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectReceiverTest.java
new file mode 100644
index 0000000..8b72aa6
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectReceiverTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests that validate Receiver behavior after a client reconnection.
+ */
+@Timeout(20)
+class ReconnectReceiverTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ReconnectReceiverTest.class);
+
+    @Test
+    public void testOpenedReceiverRecoveredAfterConnectionDroppedCreditWindow() throws Exception {
+        doTestOpenedReceiverRecoveredAfterConnectionDropped(false);
+    }
+
+    @Test
+    public void testOpenedReceiverRecoveredAfterConnectionDroppedFixedCreditGrant() throws Exception {
+        doTestOpenedReceiverRecoveredAfterConnectionDropped(true);
+    }
+
+    private void doTestOpenedReceiverRecoveredAfterConnectionDropped(boolean fixedCredit) throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            final int FIXED_CREDIT = 25;
+            final int CREDIT_WINDOW = 15;
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofReceiver().withSource().withAddress("test").and().respond();
+            if (fixedCredit) {
+                firstPeer.expectFlow().withLinkCredit(FIXED_CREDIT);
+            } else {
+                firstPeer.expectFlow().withLinkCredit(CREDIT_WINDOW);
+            }
+            firstPeer.dropAfterLastHandler(5);
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofReceiver().withSource().withAddress("test").and().respond();
+            if (fixedCredit) {
+                finalPeer.expectFlow().withLinkCredit(FIXED_CREDIT);
+            } else {
+                finalPeer.expectFlow().withLinkCredit(CREDIT_WINDOW);
+            }
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession();
+            ReceiverOptions receiverOptions = new ReceiverOptions();
+            if (fixedCredit) {
+                receiverOptions.creditWindow(0);
+            } else {
+                receiverOptions.creditWindow(CREDIT_WINDOW);
+            }
+
+            Receiver receiver = session.openReceiver("test", receiverOptions);
+            if (fixedCredit) {
+                receiver.addCredit(FIXED_CREDIT);
+            }
+
+            firstPeer.waitForScriptToComplete();
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectDetach().withClosed(true).respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            receiver.close();
+            session.close();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testDynamicReceiverLinkNotRecovered() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofReceiver()
+                                    .withSource().withDynamic(true).withAddress((String) null)
+                                    .and().respond()
+                                    .withSource().withDynamic(true).withAddress("test-dynamic-node");
+            firstPeer.dropAfterLastHandler(5);
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession();
+            ReceiverOptions receiverOptions = new ReceiverOptions().creditWindow(0);
+            Receiver receiver = session.openDynamicReceiver(receiverOptions);
+
+            firstPeer.waitForScriptToComplete();
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            try {
+                receiver.drain();
+                fail("Should not be able to drain as dynamic receiver not recovered");
+            } catch (ClientConnectionRemotelyClosedException ex) {
+                LOG.trace("Error caught: ", ex);
+            }
+
+            receiver.close();
+            session.close();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testDispositionFromDeliveryReceivedBeforeDisconnectIsNoOp() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            firstPeer.expectFlow().withLinkCredit(10);
+            firstPeer.remoteTransfer().withHandle(0)
+                                      .withDeliveryId(0)
+                                      .withDeliveryTag(new byte[] { 1 })
+                                      .withMore(false)
+                                      .withSettled(true)
+                                      .withMessageFormat(0)
+                                      .withPayload(payload).queue();
+            firstPeer.dropAfterLastHandler(100);
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            finalPeer.expectFlow().withLinkCredit(9);
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession();
+            ReceiverOptions rcvOpts = new ReceiverOptions().autoAccept(false);
+            Receiver receiver = session.openReceiver("test-queue", rcvOpts);
+            Delivery delivery = receiver.receive(10, TimeUnit.SECONDS);
+
+            firstPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectDetach().respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            assertNotNull(delivery);
+
+            delivery.accept();
+
+            receiver.close();
+            session.close();
+            connection.close();
+
+            assertNotNull(delivery);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectSenderTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectSenderTest.java
new file mode 100644
index 0000000..ec283b3
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectSenderTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests that validate Sender behavior after a client reconnection.
+ */
+@Timeout(20)
+class ReconnectSenderTest extends ImperativeClientTestCase {
+
+    @Test
+    public void testOpenedSenderRecoveredAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+            firstPeer.dropAfterLastHandler(5);
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test");
+
+            firstPeer.waitForScriptToComplete();
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectDetach().withClosed(true).respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            sender.close();
+            session.close();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testInFlightSendFailedAfterConnectionDroppedAndNotResent() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+           firstPeer.expectSASLAnonymousConnect();
+           firstPeer.expectOpen().respond();
+           firstPeer.expectBegin().respond();
+           firstPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+           firstPeer.remoteFlow().withLinkCredit(1).queue();
+           firstPeer.expectTransfer().withNonNullPayload();
+           firstPeer.dropAfterLastHandler(15);
+           firstPeer.start();
+
+           finalPeer.expectSASLAnonymousConnect();
+           finalPeer.expectOpen().respond();
+           finalPeer.expectBegin().respond();
+           finalPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+           finalPeer.start();
+
+           final URI primaryURI = firstPeer.getServerURI();
+           final URI backupURI = finalPeer.getServerURI();
+
+           ConnectionOptions options = new ConnectionOptions();
+           options.reconnectOptions().reconnectEnabled(true);
+           options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+           Client container = Client.create();
+           Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+           Session session = connection.openSession();
+           Sender sender = session.openSender("test");
+
+           final AtomicReference<Tracker> tracker = new AtomicReference<>();
+           final AtomicReference<ClientException> error = new AtomicReference<>();
+           final CountDownLatch latch = new CountDownLatch(1);
+
+           ForkJoinPool.commonPool().execute(() -> {
+               try {
+                   tracker.set(sender.send(Message.create("Hello")));
+               } catch (ClientException e) {
+                   error.set(e);
+               } finally {
+                   latch.countDown();
+               }
+           });
+
+           firstPeer.waitForScriptToComplete();
+           finalPeer.waitForScriptToComplete();
+           finalPeer.expectDetach().withClosed(true).respond();
+           finalPeer.expectEnd().respond();
+           finalPeer.expectClose().respond();
+
+           assertTrue(latch.await(10, TimeUnit.SECONDS), "Should have failed previously sent message");
+           assertNotNull(tracker.get());
+           assertNull(error.get());
+           assertThrows(ClientConnectionRemotelyClosedException.class, () -> tracker.get().awaitSettlement());
+
+           sender.close();
+           session.close();
+           connection.close();
+
+           finalPeer.waitForScriptToComplete();
+       }
+    }
+
+    @Test
+    public void testSendBlockedOnCreditGetsSentAfterReconnectAndCreditGranted() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+           firstPeer.expectSASLAnonymousConnect();
+           firstPeer.expectOpen().respond();
+           firstPeer.expectBegin().respond();
+           firstPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+           firstPeer.dropAfterLastHandler(15);
+           firstPeer.start();
+
+           finalPeer.expectSASLAnonymousConnect();
+           finalPeer.expectOpen().respond();
+           finalPeer.expectBegin().respond();
+           finalPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+           finalPeer.start();
+
+           final URI primaryURI = firstPeer.getServerURI();
+           final URI backupURI = finalPeer.getServerURI();
+
+           ConnectionOptions options = new ConnectionOptions();
+           options.reconnectOptions().reconnectEnabled(true);
+           options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+           Client container = Client.create();
+           Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+           Session session = connection.openSession();
+           Sender sender = session.openSender("test");
+
+           final AtomicReference<Tracker> tracker = new AtomicReference<>();
+           final AtomicReference<Exception> sendError = new AtomicReference<>();
+           final CountDownLatch latch = new CountDownLatch(1);
+
+           ForkJoinPool.commonPool().execute(() -> {
+               try {
+                   tracker.set(sender.send(Message.create("Hello")));
+               } catch (ClientException e) {
+                   sendError.set(e);
+               } finally {
+                   latch.countDown();
+               }
+           });
+
+           firstPeer.waitForScriptToComplete();
+           finalPeer.waitForScriptToComplete();
+           finalPeer.expectTransfer().withNonNullPayload()
+                                     .respond()
+                                     .withSettled(true).withState().accepted();
+           finalPeer.expectDetach().withClosed(true).respond();
+           finalPeer.expectEnd().respond();
+           finalPeer.expectClose().respond();
+
+           // Grant credit now and await expected message send.
+           finalPeer.remoteFlow().withDeliveryCount(0)
+                                 .withLinkCredit(10)
+                                 .withIncomingWindow(10)
+                                 .withOutgoingWindow(10)
+                                 .withNextIncomingId(0)
+                                 .withNextOutgoingId(1).now();
+
+           assertTrue(latch.await(10, TimeUnit.SECONDS), "Should have sent blocked message");
+           assertNull(sendError.get());
+           assertNotNull(tracker.get());
+
+           Tracker send = tracker.get();
+           assertSame(tracker.get(), send.awaitSettlement(10, TimeUnit.SECONDS));
+           assertTrue(send.remoteSettled());
+           assertEquals(DeliveryState.accepted(), send.remoteState());
+
+           sender.close();
+           session.close();
+           connection.close();
+
+           finalPeer.waitForScriptToComplete();
+       }
+    }
+
+    @Test
+    public void testAwaitSettlementOnSendFiredBeforeConnectionDrops() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+           firstPeer.expectSASLAnonymousConnect();
+           firstPeer.expectOpen().respond();
+           firstPeer.expectBegin().respond();
+           firstPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+           firstPeer.remoteFlow().withLinkCredit(1).queue();
+           firstPeer.expectTransfer().withNonNullPayload();
+           firstPeer.dropAfterLastHandler(15);
+           firstPeer.start();
+
+           finalPeer.expectSASLAnonymousConnect();
+           finalPeer.expectOpen().respond();
+           finalPeer.expectBegin().respond();
+           finalPeer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+           finalPeer.start();
+
+           final URI primaryURI = firstPeer.getServerURI();
+           final URI backupURI = finalPeer.getServerURI();
+
+           ConnectionOptions options = new ConnectionOptions();
+           options.reconnectOptions().reconnectEnabled(true);
+           options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+           Client container = Client.create();
+           Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+           Session session = connection.openSession();
+           Sender sender = session.openSender("test");
+           Tracker tracker = sender.send(Message.create("Hello"));
+
+           firstPeer.waitForScriptToComplete();
+           finalPeer.waitForScriptToComplete();
+           finalPeer.expectDetach().withClosed(true).respond();
+           finalPeer.expectEnd().respond();
+           finalPeer.expectClose().respond();
+
+           try {
+               tracker.awaitSettlement(120, TimeUnit.SECONDS);
+               fail("Should not be able to successfully await settlement");
+           } catch (ClientConnectionRemotelyClosedException crce) {}
+
+           assertFalse(tracker.remoteSettled());
+           assertNull(tracker.remoteState());
+
+           sender.close();
+           session.close();
+           connection.close();
+
+           finalPeer.waitForScriptToComplete();
+       }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectSessionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectSessionTest.java
new file mode 100644
index 0000000..a4aef99
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectSessionTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import java.net.URI;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests that validate client Session behavior after a client reconnection.
+ */
+@Timeout(20)
+class ReconnectSessionTest extends ImperativeClientTestCase {
+
+    @Test
+    public void testOpenedSessionRecoveredAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.dropAfterLastHandler(5);
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession().openFuture().get();
+
+            firstPeer.waitForScriptToComplete();
+
+            connection.openFuture().get();
+
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            session.close();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectStreamReceiverTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectStreamReceiverTest.java
new file mode 100644
index 0000000..48cbe33
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectStreamReceiverTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.StreamDelivery;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests that validate Stream Receiver behavior after a client reconnection.
+ */
+@Timeout(20)
+class ReconnectStreamReceiverTest extends ImperativeClientTestCase {
+
+    @Test
+    public void testStreamReceiverRecoversAndDeliveryReceived() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofReceiver().respond();
+            firstPeer.expectFlow();
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofReceiver().respond();
+            finalPeer.expectFlow();
+            finalPeer.remoteTransfer().withHandle(0)
+                                      .withDeliveryId(0)
+                                      .withDeliveryTag(new byte[] { 1 })
+                                      .withMore(false)
+                                      .withMessageFormat(0)
+                                      .withPayload(payload).queue();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            firstPeer.waitForScriptToComplete();
+            finalPeer.waitForScriptToComplete();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            finalPeer.expectDetach().respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotReceiveFromStreamStartedBeforeReconnection() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofReceiver().respond();
+            firstPeer.expectFlow();
+            firstPeer.remoteTransfer().withHandle(0)
+                                      .withDeliveryId(0)
+                                      .withDeliveryTag(new byte[] { 1 })
+                                      .withMore(true)
+                                      .withMessageFormat(0)
+                                      .withPayload(payload).queue();
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofReceiver().respond();
+            finalPeer.expectFlow();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            firstPeer.waitForScriptToComplete();
+            finalPeer.waitForScriptToComplete();
+
+            assertNotNull(delivery);
+            assertFalse(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            assertThrows(IOException.class, () -> delivery.rawInputStream().read());
+
+            finalPeer.expectDetach().respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectStreamSenderTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectStreamSenderTest.java
new file mode 100644
index 0000000..818bfc5
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectStreamSenderTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.OutputStreamOptions;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderMessage;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedDataMatcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests that validate Stream Sender behavior after a client reconnection.
+ */
+@Timeout(20)
+class ReconnectStreamSenderTest extends ImperativeClientTestCase {
+
+    @Test
+    void testStreamMessageFlushFailsAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofSender().respond();
+            firstPeer.remoteFlow().withLinkCredit(1).queue();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofSender().respond();
+            finalPeer.remoteFlow().withLinkCredit(1).queue();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            OutputStream stream = message.body();
+
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(new byte[] { 4, 5, 6, 7 });
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            firstPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            firstPeer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            firstPeer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            firstPeer.dropAfterLastHandler();
+
+            // Write two then after connection drops the message should fail on future writes
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.write(new byte[] { 4, 5, 6, 7 });
+            stream.flush();
+
+            firstPeer.waitForScriptToComplete();
+            // Reconnection should have occurred now and we should not be able to flush data from
+            // the stream as its initial sender instance was closed on disconnect.
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectClose().respond();
+
+            // Next write should fail as connection should have dropped.
+            stream.write(new byte[] { 8, 9, 10, 11 });
+
+            try {
+                stream.flush();
+                fail("Should not be able to flush after connection drop");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageCloseThatFlushesFailsAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+           firstPeer.expectSASLAnonymousConnect();
+           firstPeer.expectOpen().respond();
+           firstPeer.expectBegin().respond();
+           firstPeer.expectAttach().ofSender().respond();
+           firstPeer.remoteFlow().withLinkCredit(1).queue();
+           firstPeer.start();
+
+           finalPeer.expectSASLAnonymousConnect();
+           finalPeer.expectOpen().respond();
+           finalPeer.expectBegin().respond();
+           finalPeer.expectAttach().ofSender().respond();
+           finalPeer.remoteFlow().withLinkCredit(1).queue();
+           finalPeer.start();
+
+           final URI primaryURI = firstPeer.getServerURI();
+           final URI backupURI = finalPeer.getServerURI();
+
+           ConnectionOptions options = new ConnectionOptions();
+           options.reconnectOptions().reconnectEnabled(true);
+           options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+           Client container = Client.create();
+           Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+           StreamSender sender = connection.openStreamSender("test-queue");
+           StreamSenderMessage message = sender.beginMessage();
+
+            OutputStream stream = message.body();
+
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(new byte[] { 4, 5, 6, 7 });
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            firstPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            firstPeer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            firstPeer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            firstPeer.dropAfterLastHandler();
+
+            // Write two then after connection drops the message should fail on future writes
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.write(new byte[] { 4, 5, 6, 7 });
+            stream.flush();
+
+            firstPeer.waitForScriptToComplete();
+
+            // Reconnection should have occurred now and we should not be able to flush data from
+            // the stream as its initial sender instance was closed on disconnect.
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectClose().respond();
+
+            // Next write should fail as connection should have dropped.
+            stream.write(new byte[] { 8, 9, 10, 11 });
+
+            try {
+                stream.close();
+                fail("Should not be able to close after connection drop");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageWriteThatFlushesFailsAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofSender().respond();
+            firstPeer.remoteFlow().withLinkCredit(1).queue();
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofSender().respond();
+            finalPeer.remoteFlow().withLinkCredit(1).queue();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.maxFrameSize(32768);
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            byte[] payload = new byte[65536];
+            Arrays.fill(payload, (byte) 65);
+            OutputStreamOptions streamOptions = new OutputStreamOptions().bodyLength(payload.length);
+            OutputStream stream = message.body(streamOptions);
+
+            firstPeer.waitForScriptToComplete();
+
+            // Reconnection should have occurred now and we should not be able to flush data from
+            // the stream as its initial sender instance was closed on disconnect.
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectClose().respond();
+
+            try {
+                stream.write(payload);
+                fail("Should not be able to write section after connection drop");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamSenderRecoveredAfterReconnectCanCreateAndStreamBytes() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.expectAttach().ofSender().respond();
+            firstPeer.remoteFlow().withLinkCredit(1).queue();
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectAttach().ofSender().respond();
+            finalPeer.remoteFlow().withLinkCredit(1).queue();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            StreamSender sender = connection.openStreamSender("test-queue");
+
+            firstPeer.waitForScriptToComplete();
+
+            // After reconnection a new stream sender message should be properly created
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            finalPeer.expectTransfer().withMore(true).withPayload(new byte[] { 0, 1, 2, 3 });
+            finalPeer.expectTransfer().withMore(false).withNullPayload();
+            finalPeer.expectDetach().respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.expectClose().respond();
+
+            StreamSenderMessage message = sender.beginMessage();
+            OutputStream stream = message.rawOutputStream();
+
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectTest.java
new file mode 100644
index 0000000..9c76015
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectTest.java
@@ -0,0 +1,716 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecurityException;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecuritySaslException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.client.util.StopWatch;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.types.security.SaslCode;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test client implementation of basic connection recovery and its configuration.
+ */
+@Timeout(20)
+public class ReconnectTest extends ImperativeClientTestCase {
+
+    @Test
+    public void testConnectionNotifiesReconnectionLifecycleEvents() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.dropAfterLastHandler(5);
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            final CountDownLatch connected = new CountDownLatch(1);
+            final CountDownLatch disconnected = new CountDownLatch(1);
+            final CountDownLatch reconnected = new CountDownLatch(1);
+            final CountDownLatch failed = new CountDownLatch(1);
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().maxReconnectAttempts(5);
+            options.reconnectOptions().reconnectDelay(10);
+            options.reconnectOptions().useReconnectBackOff(false);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+            options.connectedHandler((connection, context) -> {
+                connected.countDown();
+            });
+            options.interruptedHandler((connection, context) -> {
+                disconnected.countDown();
+            });
+            options.reconnectedHandler((connection, context) -> {
+                reconnected.countDown();
+            });
+            options.disconnectedHandler((connection, context) -> {
+                failed.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+
+            firstPeer.waitForScriptToComplete();
+
+            connection.openFuture().get();
+
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectBegin().respond();
+            finalPeer.expectEnd().respond();
+            finalPeer.dropAfterLastHandler(10);
+
+            Session session = connection.openSession().openFuture().get();
+
+            session.close();
+
+            finalPeer.waitForScriptToComplete();
+
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+            assertTrue(disconnected.await(5, TimeUnit.SECONDS));
+            assertTrue(reconnected.await(5, TimeUnit.SECONDS));
+            assertTrue(failed.await(5, TimeUnit.SECONDS));
+
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testConnectThrowsSecurityViolationOnFailureSaslAuth() throws Exception {
+        doTestConnectThrowsSecurityViolationOnFailuredSaslExchange(SaslCode.AUTH.byteValue());
+    }
+
+    @Test
+    public void testConnectThrowsSecurityViolationOnFailureSaslSys() throws Exception {
+        doTestConnectThrowsSecurityViolationOnFailuredSaslExchange(SaslCode.SYS.byteValue());
+    }
+
+    @Test
+    public void testConnectThrowsSecurityViolationOnFailureSaslSysPerm() throws Exception {
+        doTestConnectThrowsSecurityViolationOnFailuredSaslExchange(SaslCode.SYS_PERM.byteValue());
+    }
+
+    private void doTestConnectThrowsSecurityViolationOnFailuredSaslExchange(byte saslCode) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectFailingSASLPlainConnect(saslCode);
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.user("test");
+            options.password("pass");
+
+            URI remoteURI = peer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            try {
+                connection.openFuture().get();
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecuritySaslException);
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReconnectStopsAfterSaslAuthFailure() throws Exception {
+        testReconnectStopsAfterSaslPermFailure(SaslCode.AUTH.byteValue());
+    }
+
+    @Test
+    public void testReconnectStopsAfterSaslSysFailure() throws Exception {
+        testReconnectStopsAfterSaslPermFailure(SaslCode.SYS.byteValue());
+    }
+
+    @Test
+    public void testReconnectStopsAfterSaslPermFailure() throws Exception {
+        testReconnectStopsAfterSaslPermFailure(SaslCode.SYS_PERM.byteValue());
+    }
+
+    private void testReconnectStopsAfterSaslPermFailure(byte saslCode) throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer secondPeer = new ProtonTestServer();
+             ProtonTestServer thirdPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            secondPeer.expectSASLAnonymousConnect();
+            secondPeer.expectOpen();
+            secondPeer.dropAfterLastHandler();
+            secondPeer.start();
+
+            thirdPeer.expectFailingSASLPlainConnect(saslCode);
+            thirdPeer.dropAfterLastHandler();
+            thirdPeer.start();
+
+            final CountDownLatch connected = new CountDownLatch(1);
+            final CountDownLatch disconnected = new CountDownLatch(1);
+            final CountDownLatch reconnected = new CountDownLatch(1);
+            final CountDownLatch failed = new CountDownLatch(1);
+
+            final URI firstURI = firstPeer.getServerURI();
+            final URI secondURI = secondPeer.getServerURI();
+            final URI thirdURI = thirdPeer.getServerURI();
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.user("test");
+            options.password("pass");
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(secondURI.getHost(), secondURI.getPort())
+                                      .addReconnectHost(thirdURI.getHost(), thirdURI.getPort());
+            options.connectedHandler((connection, context) -> {
+                connected.countDown();
+            });
+            options.interruptedHandler((connection, context) -> {
+                disconnected.countDown();
+            });
+            options.reconnectedHandler((connection, context) -> {
+                reconnected.countDown();  // This one should not be triggered
+            });
+            options.disconnectedHandler((connection, context) -> {
+                failed.countDown();
+            });
+
+            Connection connection = container.connect(firstURI.getHost(), firstURI.getPort(), options);
+
+            connection.openFuture().get();
+
+            firstPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            secondPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            thirdPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Should connect, then fail and attempt to connect to second and third before stopping
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+            assertTrue(disconnected.await(5, TimeUnit.SECONDS));
+            assertTrue(failed.await(5, TimeUnit.SECONDS));
+            assertEquals(1, reconnected.getCount());
+
+            connection.close();
+
+            thirdPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectHandlesSaslTempFailureAndReconnects() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectFailingSASLPlainConnect(SaslCode.SYS_TEMP.byteValue());
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            finalPeer.expectSASLPlainConnect("test", "pass");
+            finalPeer.expectOpen().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            final CountDownLatch connected = new CountDownLatch(1);
+            final AtomicReference<String> connectedHost = new AtomicReference<>();
+            final AtomicReference<Integer> connectedPort = new AtomicReference<>();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.user("test");
+            options.password("pass");
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+            options.connectedHandler((connection, event) -> {
+                connectedHost.set(event.host());
+                connectedPort.set(event.port());
+                connected.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+
+            firstPeer.waitForScriptToComplete();
+
+            connection.openFuture().get();
+
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+
+            // Should never have connected and exchanged Open performatives with first peer
+            // so we won't have had a connection established event there.
+            assertEquals(backupURI.getHost(), connectedHost.get());
+            assertEquals(backupURI.getPort(), connectedPort.get());
+
+            finalPeer.waitForScriptToComplete();
+
+            finalPeer.expectClose().respond();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testConnectThrowsSecurityViolationOnFailureFromOpen() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().reject(AmqpError.UNAUTHORIZED_ACCESS.toString(), "Anonymous connections not allowed");
+            peer.expectBegin().optional();  // Could arrive if remote open response not processed in time
+            peer.expectClose();
+            peer.start();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+
+            URI remoteURI = peer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            try {
+                connection.openFuture().get();
+            } catch (ExecutionException exe) {
+                // Possible based on time of rejecting open arrival.
+                assertTrue(exe.getCause() instanceof ClientConnectionSecurityException);
+            }
+
+            try {
+                connection.defaultSession().openFuture().get();
+                fail("Should fail connection since remote rejected open with auth error");
+            } catch (ClientConnectionSecurityException cliEx) {
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecurityException);
+            }
+
+            connection.close();
+
+            try {
+                connection.defaultSession();
+                fail("Should fail as illegal state as connection was closed.");
+            } catch (ClientIllegalStateException exe) {
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReconnectHandlesDropThenRejectionCloseAfterConnect() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+            ProtonTestServer secondPeer = new ProtonTestServer();
+            ProtonTestServer thirdPeer = new ProtonTestServer()) {
+
+           firstPeer.expectSASLAnonymousConnect();
+           firstPeer.expectOpen().respond();
+           firstPeer.start();
+
+           secondPeer.expectSASLAnonymousConnect();
+           secondPeer.expectOpen().reject(AmqpError.INVALID_FIELD.toString(), "Connection configuration has invalid field");
+           secondPeer.expectClose();
+           secondPeer.start();
+
+           thirdPeer.expectSASLAnonymousConnect();
+           thirdPeer.expectOpen().respond();
+           thirdPeer.start();
+
+           final CountDownLatch connected = new CountDownLatch(1);
+           final CountDownLatch disconnected = new CountDownLatch(2);
+           final CountDownLatch reconnected = new CountDownLatch(2);
+           final CountDownLatch failed = new CountDownLatch(1);
+
+           final URI firstURI = firstPeer.getServerURI();
+           final URI secondURI = secondPeer.getServerURI();
+           final URI thirdURI = thirdPeer.getServerURI();
+
+           ConnectionOptions options = new ConnectionOptions();
+           options.reconnectOptions().reconnectEnabled(true);
+           options.reconnectOptions().addReconnectHost(secondURI.getHost(), secondURI.getPort())
+                                     .addReconnectHost(thirdURI.getHost(), thirdURI.getPort());
+           options.connectedHandler((connection, context) -> {
+               connected.countDown();
+           });
+           options.interruptedHandler((connection, context) -> {
+               disconnected.countDown();
+           });
+           options.reconnectedHandler((connection, context) -> {
+               reconnected.countDown();
+           });
+           options.disconnectedHandler((connection, context) -> {
+               failed.countDown();  // Not expecting any failure in this test case
+           });
+
+           Connection connection = Client.create().connect(firstURI.getHost(), firstURI.getPort(), options);
+
+           firstPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+           connection.openFuture().get();
+
+           firstPeer.close();
+
+           secondPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+           // Should connect, then fail and attempt to connect to second and be rejected then reconnect to third.
+           assertTrue(connected.await(5, TimeUnit.SECONDS));
+           assertTrue(disconnected.await(5, TimeUnit.SECONDS));
+           assertTrue(reconnected.await(5, TimeUnit.SECONDS));
+           assertEquals(1, failed.getCount());
+
+           thirdPeer.expectClose().respond();
+           connection.close();
+
+           thirdPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testClientReconnectsWhenConnectionDropsAfterOpenReceived() throws Exception {
+        doTestClientReconnectsWhenConnectionDropsAfterOpenReceived(0);
+    }
+
+    @Test
+    public void testClientReconnectsWhenConnectionDropsAfterDelayAfterOpenReceived() throws Exception {
+        doTestClientReconnectsWhenConnectionDropsAfterOpenReceived(20);
+    }
+
+    private void doTestClientReconnectsWhenConnectionDropsAfterOpenReceived(int dropDelay) throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen();
+            if (dropDelay > 0) {
+                firstPeer.dropAfterLastHandler(dropDelay);
+            } else {
+                firstPeer.dropAfterLastHandler();
+            }
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            final CountDownLatch connected = new CountDownLatch(1);
+            final AtomicReference<String> connectedHost = new AtomicReference<>();
+            final AtomicReference<Integer> connectedPort = new AtomicReference<>();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+            options.connectedHandler((connection, event) -> {
+                connectedHost.set(event.host());
+                connectedPort.set(event.port());
+                connected.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+
+            firstPeer.waitForScriptToComplete();
+
+            connection.openFuture().get();
+
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+            assertEquals(backupURI.getHost(), connectedHost.get());
+            assertEquals(backupURI.getPort(), connectedPort.get());
+
+            finalPeer.waitForScriptToComplete();
+
+            finalPeer.expectClose().respond();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testClientReconnectsWhenOpenRejected() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().reject(AmqpError.INVALID_FIELD.toString(), "Error with client Open performative");
+            firstPeer.expectClose();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            final CountDownLatch connected = new CountDownLatch(1);
+            final AtomicReference<String> connectedHost = new AtomicReference<>();
+            final AtomicReference<Integer> connectedPort = new AtomicReference<>();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+            options.connectedHandler((connection, event) -> {
+                connectedHost.set(event.host());
+                connectedPort.set(event.port());
+                connected.countDown();
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+
+            firstPeer.waitForScriptToComplete();
+
+            connection.openFuture().get();
+
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+            assertEquals(primaryURI.getHost(), connectedHost.get());
+            assertEquals(primaryURI.getPort(), connectedPort.get());
+
+            finalPeer.waitForScriptToComplete();
+
+            finalPeer.expectClose().respond();
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testClientReconnectsWhenConnectionRemotelyClosedWithForced() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin();
+            firstPeer.remoteClose().withErrorCondition(ConnectionError.CONNECTION_FORCED.toString(), "Forced disconnect").queue();
+            firstPeer.expectClose();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            final CountDownLatch connected = new CountDownLatch(1);
+            final CountDownLatch disconnected = new CountDownLatch(1);
+            final CountDownLatch reconnected = new CountDownLatch(1);
+            final CountDownLatch failed = new CountDownLatch(1);
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+            options.connectedHandler((connection, context) -> {
+                connected.countDown();
+            });
+            options.interruptedHandler((connection, context) -> {
+                disconnected.countDown();
+            });
+            options.reconnectedHandler((connection, context) -> {
+                reconnected.countDown();
+            });
+            options.disconnectedHandler((connection, context) -> {
+                failed.countDown();  // Not expecting any failure in this test case
+            });
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession();
+
+            connection.openFuture().get();
+
+            firstPeer.waitForScriptToComplete();
+
+            try {
+                session.openFuture().get();
+            } catch (Exception ex) {
+                fail("Should eventually succeed in opening this Session");
+            }
+
+            // Should connect, then be remotely closed and reconnect to the alternate
+            assertTrue(connected.await(5, TimeUnit.SECONDS));
+            assertTrue(disconnected.await(5, TimeUnit.SECONDS));
+            assertTrue(reconnected.await(5, TimeUnit.SECONDS));
+            assertEquals(1, failed.getCount());
+
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectClose().respond();
+
+            connection.close();
+
+            finalPeer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void testInitialReconnectDelayDoesNotApplyToInitialConnect() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+
+            final URI remoteURI = peer.getServerURI();
+            final int delay = 20000;
+            final StopWatch watch = new StopWatch();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+
+            connection.openFuture().get();
+
+            long taken = watch.taken();
+
+            final String message = "Initial connect should not have delayed for the specified initialReconnectDelay." +
+                                   "Elapsed=" + taken + ", delay=" + delay;
+            assertTrue(taken < delay, message);
+            assertTrue(taken < 5000, "Connection took longer than reasonable: " + taken);
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionReportsFailedAfterMaxinitialReconnectAttempts() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer()) {
+            firstPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+
+            firstPeer.close();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().maxReconnectAttempts(-1); // Try forever if connect succeeds once.
+            options.reconnectOptions().maxInitialConnectionAttempts(3);
+            options.reconnectOptions().warnAfterReconnectAttempts(5);
+            options.reconnectOptions().reconnectDelay(10);
+            options.reconnectOptions().useReconnectBackOff(false);
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+
+            try {
+                connection.openFuture().get();
+                fail("Should not successfully connect.");
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionRemotelyClosedException);
+            }
+
+            try {
+                connection.defaultSender();
+                fail("Connection should be in a failed state now.");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+            }
+
+            connection.close();
+
+            try {
+                connection.defaultSender();
+                fail("Connection should be in a closed state now.");
+            } catch (ClientIllegalStateException cliEx) {
+            }
+        }
+    }
+
+    @Test
+    public void testConnectionReportsFailedAfterMaxinitialReconnectAttemptsWithBackOff() throws Exception {
+        try (ProtonTestServer firstPeer = new ProtonTestServer()) {
+            firstPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+
+            firstPeer.close();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().maxReconnectAttempts(-1); // Try forever if connect succeeds once.
+            options.reconnectOptions().maxInitialConnectionAttempts(10);
+            options.reconnectOptions().warnAfterReconnectAttempts(2);
+            options.reconnectOptions().reconnectDelay(10);
+            options.reconnectOptions().useReconnectBackOff(true);
+            options.reconnectOptions().maxReconnectDelay(100);
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+
+            try {
+                connection.openFuture().get();
+                fail("Should not successfully connect.");
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionRemotelyClosedException);
+            }
+
+            try {
+                connection.defaultSender();
+                fail("Connection should be in a failed state now.");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+            }
+
+            connection.close();
+
+            try {
+                connection.defaultSender();
+                fail("Connection should be in a closed state now.");
+            } catch (ClientIllegalStateException cliEx) {
+            }
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectTransactionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectTransactionTest.java
new file mode 100644
index 0000000..a96c302
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/ReconnectTransactionTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test client implementation with Transactions and connection recovery
+ */
+@Timeout(20)
+class ReconnectTransactionTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ReconnectTransactionTest.class);
+
+    @Test
+    public void testDeclareTransactionAfterConnectionDropsAndReconnects() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer firstPeer = new ProtonTestServer();
+             ProtonTestServer finalPeer = new ProtonTestServer()) {
+
+            firstPeer.expectSASLAnonymousConnect();
+            firstPeer.expectOpen().respond();
+            firstPeer.expectBegin().respond();
+            firstPeer.dropAfterLastHandler();
+            firstPeer.start();
+
+            finalPeer.expectSASLAnonymousConnect();
+            finalPeer.expectOpen().respond();
+            finalPeer.expectBegin().respond();
+            finalPeer.start();
+
+            final URI primaryURI = firstPeer.getServerURI();
+            final URI backupURI = finalPeer.getServerURI();
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.reconnectOptions().reconnectEnabled(true);
+            options.reconnectOptions().addReconnectHost(backupURI.getHost(), backupURI.getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(primaryURI.getHost(), primaryURI.getPort(), options);
+            Session session = connection.openSession().openFuture().get();
+
+            firstPeer.waitForScriptToComplete();
+
+            finalPeer.waitForScriptToComplete();
+            finalPeer.expectCoordinatorAttach().respond();
+            finalPeer.remoteFlow().withLinkCredit(2).queue();
+            finalPeer.expectDeclare().accept(txnId);
+            finalPeer.expectClose().respond();
+
+            try {
+                session.beginTransaction();
+            } catch (ClientException cliEx) {
+                LOG.info("Caught unexpected error from test", cliEx);
+                fail("Should not have failed to declare transaction");
+            }
+
+            connection.closeAsync().get();
+
+            finalPeer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java
new file mode 100644
index 0000000..73af0a0
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java
@@ -0,0 +1,454 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionSecuritySaslException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Timeout(20)
+public class SaslConnectionTest extends ImperativeClientTestCase {
+
+    private static final String ANONYMOUS = "ANONYMOUS";
+    private static final String PLAIN = "PLAIN";
+    private static final String CRAM_MD5 = "CRAM-MD5";
+    private static final String SCRAM_SHA_1 = "SCRAM-SHA-1";
+    private static final String SCRAM_SHA_256 = "SCRAM-SHA-256";
+    private static final String SCRAM_SHA_512 = "SCRAM-SHA-512";
+    private static final String EXTERNAL = "EXTERNAL";
+    private static final String XOAUTH2 = "XOAUTH2";
+
+    private static final UnsignedByte SASL_FAIL_AUTH = UnsignedByte.valueOf((byte) 1);
+    private static final UnsignedByte SASL_SYS = UnsignedByte.valueOf((byte) 2);
+    private static final UnsignedByte SASL_SYS_PERM = UnsignedByte.valueOf((byte) 3);
+    private static final UnsignedByte SASL_SYS_TEMP = UnsignedByte.valueOf((byte) 4);
+
+    private static final String BROKER_JKS_KEYSTORE = "src/test/resources/broker-jks.keystore";
+    private static final String BROKER_JKS_TRUSTSTORE = "src/test/resources/broker-jks.truststore";
+    private static final String CLIENT_JKS_KEYSTORE = "src/test/resources/client-jks.keystore";
+    private static final String CLIENT_JKS_TRUSTSTORE = "src/test/resources/client-jks.truststore";
+    private static final String PASSWORD = "password";
+
+    protected ProtonTestServerOptions serverOptions() {
+        return new ProtonTestServerOptions();
+    }
+
+    protected ConnectionOptions connectionOptions() {
+        return new ConnectionOptions();
+    }
+
+    @Test
+    public void testSaslLayerDisabledConnection() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.saslOptions().saslEnabled(false);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertFalse(peer.hasSecureConnection());
+            assertFalse(peer.isConnectionVerified());
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslExternalConnection() throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+        serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+        serverOptions.setTrustStorePassword(PASSWORD);
+        serverOptions.setNeedClientAuth(true);
+        serverOptions.setSecure(true);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSaslExternalConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .sslEnabled(true)
+                         .keyStoreLocation(CLIENT_JKS_KEYSTORE)
+                         .keyStorePassword(PASSWORD)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertTrue(peer.isConnectionVerified());
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslPlainConnection() throws Exception {
+        final String username = "user";
+        final String password = "qwerty123456";
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectSASLPlainConnect(username, password);
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user(username);
+            clientOptions.password(password);
+            clientOptions.traceFrames(true);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertFalse(peer.hasSecureConnection());
+            assertFalse(peer.isConnectionVerified());
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslXOauth2Connection() throws Exception {
+        final String username = "user";
+        final String password = "eyB1c2VyPSJ1c2VyIiB9";
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectSaslXOauth2Connect(username, password);
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user(username);
+            clientOptions.password(password);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslAnonymousConnection() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslFailureCodes() throws Exception {
+        doSaslFailureCodesTestImpl(SASL_FAIL_AUTH);
+        doSaslFailureCodesTestImpl(SASL_SYS);
+        doSaslFailureCodesTestImpl(SASL_SYS_PERM);
+        doSaslFailureCodesTestImpl(SASL_SYS_TEMP);
+    }
+
+    private void doSaslFailureCodesTestImpl(UnsignedByte saslFailureCode) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectFailingSASLPlainConnect(saslFailureCode.byteValue(), "PLAIN", "ANONYMOUS");
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user("username");
+            clientOptions.password("password");
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecuritySaslException);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    /**
+     * Add a small delay after the SASL process fails, test peer will throw if
+     * any unexpected frames arrive, such as erroneous open+close.
+     *
+     * @throws Exception if an error occurs during the test.
+     */
+    @Test
+    public void testWaitForUnexpectedFramesAfterSaslFailure() throws Exception {
+        doMechanismSelectedTestImpl(null, null, ANONYMOUS, new String[] {ANONYMOUS}, true);
+    }
+
+    @Test
+    public void testAnonymousSelectedWhenNoPasswordWasSupplied() throws Exception {
+        doMechanismSelectedTestImpl("username", null, ANONYMOUS, new String[] {CRAM_MD5, PLAIN, ANONYMOUS}, false);
+    }
+
+    @Test
+    public void testCramMd5SelectedWhenCredentialsPresent() throws Exception {
+        doMechanismSelectedTestImpl("username", "password", CRAM_MD5, new String[] {CRAM_MD5, PLAIN, ANONYMOUS}, false);
+    }
+
+    @Test
+    public void testScramSha1SelectedWhenCredentialsPresent() throws Exception {
+        doMechanismSelectedTestImpl("username", "password", SCRAM_SHA_1, new String[] {SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}, false);
+    }
+
+    @Test
+    public void testScramSha256SelectedWhenCredentialsPresent() throws Exception {
+        doMechanismSelectedTestImpl("username", "password", SCRAM_SHA_256, new String[] {SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}, false);
+    }
+
+    @Test
+    public void testScramSha512SelectedWhenCredentialsPresent() throws Exception {
+        doMechanismSelectedTestImpl("username", "password", SCRAM_SHA_512, new String[] {SCRAM_SHA_512, SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}, false);
+    }
+
+    @Test
+    public void testXoauth2SelectedWhenCredentialsPresent() throws Exception {
+        String token = Base64.getEncoder().encodeToString("token".getBytes(StandardCharsets.US_ASCII));
+        doMechanismSelectedTestImpl("username", token, XOAUTH2, new String[] {XOAUTH2, ANONYMOUS}, false);
+    }
+
+    private void doMechanismSelectedTestImpl(String username, String password, String clientSelectedMech, String[] serverMechs, boolean wait) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectSaslConnectThatAlwaysFailsAuthentication(serverMechs, clientSelectedMech);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user(username);
+            clientOptions.password(password);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecuritySaslException);
+            }
+
+            if (wait) {
+                Thread.sleep(200);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExternalSelectedWhenLocalPrincipalPresent() throws Exception {
+        doMechanismSelectedExternalTestImpl(true, EXTERNAL, new String[] {EXTERNAL, SCRAM_SHA_512, SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS});
+    }
+
+    @Test
+    public void testExternalNotSelectedWhenLocalPrincipalMissing() throws Exception {
+        doMechanismSelectedExternalTestImpl(false, ANONYMOUS, new String[] {EXTERNAL, SCRAM_SHA_512, SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS});
+    }
+
+    private void doMechanismSelectedExternalTestImpl(boolean requireClientCert, String clientSelectedMech, String[] serverMechs) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+        serverOptions.setSecure(true);
+        if (requireClientCert) {
+            serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+            serverOptions.setTrustStorePassword(PASSWORD);
+            serverOptions.setNeedClientAuth(requireClientCert);
+        }
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSaslConnectThatAlwaysFailsAuthentication(serverMechs, clientSelectedMech);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .sslEnabled(true)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD);
+            if (requireClientCert) {
+                clientOptions.sslOptions().keyStoreLocation(CLIENT_JKS_KEYSTORE)
+                                          .keyStorePassword(PASSWORD);
+            }
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecuritySaslException);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testRestrictSaslMechanismsWithSingleMech() throws Exception {
+        // Check PLAIN gets picked when we don't specify a restriction
+        doMechanismSelectionRestrictedTestImpl("username", "password", PLAIN, new String[] { PLAIN, ANONYMOUS}, (String) null);
+
+        // Check ANONYMOUS gets picked when we do specify a restriction
+        doMechanismSelectionRestrictedTestImpl("username", "password", ANONYMOUS, new String[] { PLAIN, ANONYMOUS}, ANONYMOUS);
+    }
+
+    @Test
+    public void testRestrictSaslMechanismsWithMultipleMechs() throws Exception {
+        // Check CRAM-MD5 gets picked when we dont specify a restriction
+        doMechanismSelectionRestrictedTestImpl("username", "password", CRAM_MD5, new String[] {CRAM_MD5, PLAIN, ANONYMOUS}, (String) null);
+
+        // Check PLAIN gets picked when we specify a restriction with multiple mechs
+        doMechanismSelectionRestrictedTestImpl("username", "password", PLAIN, new String[] { CRAM_MD5, PLAIN, ANONYMOUS}, "PLAIN", "ANONYMOUS");
+    }
+
+    @Test
+    public void testRestrictSaslMechanismsWithMultipleMechsNoPassword() throws Exception {
+        // Check ANONYMOUS gets picked when we specify a restriction with multiple mechs but don't give a password
+        doMechanismSelectionRestrictedTestImpl("username", null, ANONYMOUS, new String[] { CRAM_MD5, PLAIN, ANONYMOUS}, "PLAIN", "ANONYMOUS");
+    }
+
+    private void doMechanismSelectionRestrictedTestImpl(String username, String password, String clientSelectedMech,
+                                                        String[] serverMechs, String... allowedClientMechanisms) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectSaslConnectThatAlwaysFailsAuthentication(serverMechs, clientSelectedMech);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user(username);
+            clientOptions.password(password);
+            for (String mechanism : allowedClientMechanisms) {
+                if (mechanism != null && !mechanism.isEmpty()) {
+                    clientOptions.saslOptions().addAllowedMechanism(mechanism);
+                }
+            }
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecuritySaslException);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testMechanismNegotiationFailsToFindMatch() throws Exception {
+        String[] serverMechs = new String[] { SCRAM_SHA_1, "UNKNOWN", PLAIN};
+
+        String breadCrumb = "Could not find a suitable SASL Mechanism. " +
+                            "No supported mechanism, or none usable with the available credentials.";
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
+            peer.expectSaslMechanismNegotiationFailure(serverMechs);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            try {
+                connection.openFuture().get(10, TimeUnit.SECONDS);
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionSecuritySaslException);
+                assertTrue(exe.getCause().getMessage().contains(breadCrumb));
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SenderTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SenderTest.java
new file mode 100644
index 0000000..de4a3a0
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SenderTest.java
@@ -0,0 +1,2547 @@
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.DeliveryMode;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.DistributionMode;
+import org.apache.qpid.protonj2.client.DurabilityMode;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.ExpiryPolicy;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.Source;
+import org.apache.qpid.protonj2.client.Target;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientDeliveryStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientLinkRedirectedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientLinkRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientResourceRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientSendTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.DeliveryAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.LinkError;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Timeout(20)
+public class SenderTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SenderTest.class);
+
+    @Test
+    public void testCreateSenderAndClose() throws Exception {
+        doTestCreateSenderAndCloseOrDeatch(true);
+    }
+
+    @Test
+    public void testCreateSenderAndDetach() throws Exception {
+        doTestCreateSenderAndCloseOrDeatch(false);
+    }
+
+    private void doTestCreateSenderAndCloseOrDeatch(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                sender.closeAsync().get(10, TimeUnit.SECONDS);
+            } else {
+                sender.detachAsync().get(10, TimeUnit.SECONDS);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateSenderAndCloseSync() throws Exception {
+        doTestCreateSenderAndCloseOrDeatchSync(true);
+    }
+
+    @Test
+    public void testCreateSenderAndDetachSync() throws Exception {
+        doTestCreateSenderAndCloseOrDeatchSync(false);
+    }
+
+    private void doTestCreateSenderAndCloseOrDeatchSync(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                sender.close();
+            } else {
+                sender.detach();
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateSenderAndCloseWithErrorSync() throws Exception {
+        doTestCreateSenderAndCloseOrDeatchWithErrorSync(true);
+    }
+
+    @Test
+    public void testCreateSenderAndDetachWithErrorSync() throws Exception {
+        doTestCreateSenderAndCloseOrDeatchWithErrorSync(false);
+    }
+
+    private void doTestCreateSenderAndCloseOrDeatchWithErrorSync(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().withError("amqp-resource-deleted", "an error message").withClosed(close).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                sender.close(ErrorCondition.create("amqp-resource-deleted", "an error message", null));
+            } else {
+                sender.detach(ErrorCondition.create("amqp-resource-deleted", "an error message", null));
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSenderOpenRejectedByRemote() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().respond().withNullTarget();
+            peer.remoteDetach().withErrorCondition(AmqpError.UNAUTHORIZED_ACCESS.toString(), "Cannot read from this address").queue();
+            peer.expectDetach();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            session.openFuture().get(10, TimeUnit.SECONDS);
+
+            Sender sender = session.openSender("test-queue");
+            try {
+                sender.openFuture().get(10, TimeUnit.SECONDS);
+                fail("Open of sender should fail due to remote indicating pending close.");
+            } catch (ExecutionException exe) {
+                assertNotNull(exe.getCause());
+                assertTrue(exe.getCause() instanceof ClientLinkRemotelyClosedException);
+                ClientLinkRemotelyClosedException linkClosed = (ClientLinkRemotelyClosedException) exe.getCause();
+                assertNotNull(linkClosed.getErrorCondition());
+                assertEquals(AmqpError.UNAUTHORIZED_ACCESS.toString(), linkClosed.getErrorCondition().condition());
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Should not result in any close being sent now, already closed.
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.expectClose().respond();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testRemotelyCloseSenderLinkWithRedirect() throws Exception {
+        final String redirectVhost = "vhost";
+        final String redirectNetworkHost = "localhost";
+        final String redirectAddress = "redirect-queue";
+        final int redirectPort = 5677;
+        final String redirectScheme = "wss";
+        final String redirectPath = "/websockets";
+
+        // Tell the test peer to close the connection when executing its last handler
+        final Map<String, Object> errorInfo = new HashMap<>();
+        errorInfo.put(ClientConstants.OPEN_HOSTNAME.toString(), redirectVhost);
+        errorInfo.put(ClientConstants.NETWORK_HOST.toString(), redirectNetworkHost);
+        errorInfo.put(ClientConstants.PORT.toString(), redirectPort);
+        errorInfo.put(ClientConstants.SCHEME.toString(), redirectScheme);
+        errorInfo.put(ClientConstants.PATH.toString(), redirectPath);
+        errorInfo.put(ClientConstants.ADDRESS.toString(), redirectAddress);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond().withNullTarget();
+            peer.remoteDetach().withClosed(true)
+                               .withErrorCondition(LinkError.REDIRECT.toString(), "Not accepting links here", errorInfo).queue();
+            peer.expectDetach();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+
+            try {
+                sender.openFuture().get();
+                fail("Should not be able to create sender since the remote is redirecting.");
+            } catch (Exception ex) {
+                LOG.debug("Received expected exception from sender open: {}", ex.getMessage());
+                Throwable cause = ex.getCause();
+                assertTrue(cause instanceof ClientLinkRedirectedException);
+
+                ClientLinkRedirectedException linkRedirect = (ClientLinkRedirectedException) ex.getCause();
+
+                assertEquals(redirectVhost, linkRedirect.getHostname());
+                assertEquals(redirectNetworkHost, linkRedirect.getNetworkHost());
+                assertEquals(redirectPort, linkRedirect.getPort());
+                assertEquals(redirectScheme, linkRedirect.getScheme());
+                assertEquals(redirectPath, linkRedirect.getPath());
+                assertEquals(redirectAddress, linkRedirect.getAddress());
+
+                URI redirect = linkRedirect.getRedirectionURI();
+
+                assertEquals(redirectNetworkHost, redirect.getHost());
+                assertEquals(redirectPort, redirect.getPort());
+                assertEquals(redirectScheme, redirect.getScheme());
+                assertEquals(redirectPath, redirect.getPath());
+            }
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenSenderTimesOutWhenNoAttachResponseReceivedTimeout() throws Exception {
+        doTestOpenSenderTimesOutWhenNoAttachResponseReceived(true);
+    }
+
+    @Test
+    public void testOpenSenderTimesOutWhenNoAttachResponseReceivedNoTimeout() throws Exception {
+        doTestOpenSenderTimesOutWhenNoAttachResponseReceived(false);
+    }
+
+    private void doTestOpenSenderTimesOutWhenNoAttachResponseReceived(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender();
+            peer.expectDetach();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+            Sender sender = session.openSender("test-queue", new SenderOptions().openTimeout(10));
+
+            try {
+                if (timeout) {
+                    sender.openFuture().get(20, TimeUnit.SECONDS);
+                } else {
+                    sender.openFuture().get();
+                }
+
+                fail("Should not complete the open future without an error");
+            } catch (ExecutionException exe) {
+                Throwable cause = exe.getCause();
+                assertTrue(cause instanceof ClientOperationTimedOutException);
+            }
+
+            LOG.info("Closing connection after waiting for sender open");
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenSenderWaitWithTimeoutFailsWhenConnectionDrops() throws Exception {
+        doTestOpenSenderWaitFailsWhenConnectionDrops(true);
+    }
+
+    @Test
+    public void testOpenSenderWaitWithNoTimeoutFailsWhenConnectionDrops() throws Exception {
+        doTestOpenSenderWaitFailsWhenConnectionDrops(false);
+    }
+
+    private void doTestOpenSenderWaitFailsWhenConnectionDrops(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+
+            Thread.sleep(10);
+
+            try {
+                if (timeout) {
+                    sender.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    sender.openFuture().get();
+                }
+
+                fail("Should not complete the open future without an error");
+            } catch (ExecutionException exe) {
+                Throwable cause = exe.getCause();
+                LOG.trace("Caught exception caused by: {}", exe);
+                assertTrue(cause instanceof ClientConnectionRemotelyClosedException);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCloseSenderTimesOutWhenNoCloseResponseReceivedTimeout() throws Exception {
+        doTestCloseOrDetachSenderTimesOutWhenNoCloseResponseReceived(true, true);
+    }
+
+    @Test
+    public void testCloseSenderTimesOutWhenNoCloseResponseReceivedNoTimeout() throws Exception {
+        doTestCloseOrDetachSenderTimesOutWhenNoCloseResponseReceived(true, false);
+    }
+
+    @Test
+    public void testDetachSenderTimesOutWhenNoCloseResponseReceivedTimeout() throws Exception {
+        doTestCloseOrDetachSenderTimesOutWhenNoCloseResponseReceived(false, true);
+    }
+
+    @Test
+    public void testDetachSenderTimesOutWhenNoCloseResponseReceivedNoTimeout() throws Exception {
+        doTestCloseOrDetachSenderTimesOutWhenNoCloseResponseReceived(false, false);
+    }
+
+    private void doTestCloseOrDetachSenderTimesOutWhenNoCloseResponseReceived(boolean close, boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.closeTimeout(10);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            try {
+                if (close) {
+                    if (timeout) {
+                        sender.closeAsync().get(10, TimeUnit.SECONDS);
+                    } else {
+                        sender.closeAsync().get();
+                    }
+                } else {
+                    if (timeout) {
+                        sender.detachAsync().get(10, TimeUnit.SECONDS);
+                    } else {
+                        sender.detachAsync().get();
+                    }
+                }
+
+                fail("Should not complete the close or detach future without an error");
+            } catch (ExecutionException exe) {
+                Throwable cause = exe.getCause();
+                assertTrue(cause instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendTimesOutWhenNoCreditIssued() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.sendTimeout(1);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            Message<String> message = Message.create("Hello World");
+            try {
+                sender.send(message);
+                fail("Should throw a send timed out exception");
+            } catch (ClientSendTimedOutException ex) {
+                // Expected error, ignore
+            }
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendCompletesWhenCreditEventuallyOffered() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.sendTimeout(200);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // Expect a transfer but only after the flow which is delayed to allow the
+            // client time to block on credit.
+            peer.expectTransfer().withNonNullPayload();
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(1)
+                             .withIncomingWindow(1024)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(1).later(30);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Message<String> message = Message.create("Hello World");
+            try {
+                LOG.debug("Attempting send with sender: {}", sender);
+                sender.send(message);
+            } catch (ClientSendTimedOutException ex) {
+                fail("Should not throw a send timed out exception");
+            }
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendWhenCreditIsAvailable() throws Exception {
+        doTestSendWhenCreditIsAvailable(false, false);
+    }
+
+    @Test
+    public void testTrySendWhenCreditIsAvailable() throws Exception {
+        doTestSendWhenCreditIsAvailable(true, false);
+    }
+
+    @Test
+    public void testSendWhenCreditIsAvailableWithDeliveryAnnotations() throws Exception {
+        doTestSendWhenCreditIsAvailable(false, true);
+    }
+
+    @Test
+    public void testTrySendWhenCreditIsAvailableWithDeliveryAnnotations() throws Exception {
+        doTestSendWhenCreditIsAvailable(true, true);
+    }
+
+    private void doTestSendWhenCreditIsAvailable(boolean trySend, boolean addDeliveryAnnotations) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(10)
+                             .withIncomingWindow(1024)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(1).queue();
+            peer.expectAttach().ofReceiver().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            // This ensures that the flow to sender is processed before we try-send
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            Map<String, Object> deliveryAnnotations = new HashMap<>();
+            deliveryAnnotations.put("da1", 1);
+            deliveryAnnotations.put("da2", 2);
+            deliveryAnnotations.put("da3", 3);
+            DeliveryAnnotationsMatcher daMatcher = new DeliveryAnnotationsMatcher(true);
+            daMatcher.withEntry("da1", Matchers.equalTo(1));
+            daMatcher.withEntry("da2", Matchers.equalTo(2));
+            daMatcher.withEntry("da3", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            if (addDeliveryAnnotations) {
+                payloadMatcher.setDeliveryAnnotationsMatcher(daMatcher);
+            }
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Message<String> message = Message.create("Hello World");
+
+            final Tracker tracker;
+            if (trySend) {
+                if (addDeliveryAnnotations) {
+                    tracker = sender.trySend(message, deliveryAnnotations);
+                } else {
+                    tracker = sender.trySend(message);
+                }
+            } else {
+                if (addDeliveryAnnotations) {
+                    tracker = sender.send(message, deliveryAnnotations);
+                } else {
+                    tracker = sender.send(message);
+                }
+            }
+
+            assertNotNull(tracker);
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTrySendWhenNoCreditAvailable() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions();
+            options.sendTimeout(1);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            Message<String> message = Message.create("Hello World");
+            assertNull(sender.trySend(message));
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateSenderWithQoSOfAtMostOnce() throws Exception {
+        doTestCreateSenderWithConfiguredQoS(DeliveryMode.AT_MOST_ONCE);
+    }
+
+    @Test
+    public void testCreateSenderWithQoSOfAtLeastOnce() throws Exception {
+        doTestCreateSenderWithConfiguredQoS(DeliveryMode.AT_LEAST_ONCE);
+    }
+
+    private void doTestCreateSenderWithConfiguredQoS(DeliveryMode qos) throws Exception {
+        byte sndMode = qos == DeliveryMode.AT_MOST_ONCE ? SenderSettleMode.SETTLED.byteValue() : SenderSettleMode.UNSETTLED.byteValue();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender()
+                               .withSndSettleMode(sndMode)
+                               .withRcvSettleMode(ReceiverSettleMode.FIRST.byteValue())
+                               .respond()
+                               .withSndSettleMode(sndMode)
+                               .withRcvSettleMode(ReceiverSettleMode.FIRST.byteValue());
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+
+            SenderOptions options = new SenderOptions().deliveryMode(qos);
+            Sender sender = session.openSender("test-qos", options);
+            sender.openFuture().get();
+
+            assertEquals("test-qos", sender.address());
+
+            sender.closeAsync();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendAutoSettlesOnceRemoteSettles() throws Exception {
+        doTestSentMessageGetsAutoSettledAfterRemtoeSettles(false);
+    }
+
+    @Test
+    public void testTrySendAutoSettlesOnceRemoteSettles() throws Exception {
+        doTestSentMessageGetsAutoSettledAfterRemtoeSettles(true);
+    }
+
+    private void doTestSentMessageGetsAutoSettledAfterRemtoeSettles(boolean trySend) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(10)
+                             .withIncomingWindow(1024)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(1).queue();
+            peer.expectAttach().ofReceiver().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            // This ensures that the flow to sender is processed before we try-send
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload()
+                                 .respond()
+                                 .withSettled(true).withState().accepted();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Message<String> message = Message.create("Hello World");
+
+            final Tracker tracker;
+            if (trySend) {
+                tracker = sender.trySend(message);
+            } else {
+                tracker = sender.send(message);
+            }
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().get(5, TimeUnit.SECONDS));
+            assertEquals(tracker.remoteState().getType(), DeliveryState.Type.ACCEPTED);
+
+            sender.closeAsync();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendDoesNotAutoSettlesOnceRemoteSettlesIfAutoSettleOff() throws Exception {
+        doTestSentMessageNotAutoSettledAfterRemtoeSettles(false);
+    }
+
+    @Test
+    public void testTrySendDoesNotAutoSettlesOnceRemoteSettlesIfAutoSettleOff() throws Exception {
+        doTestSentMessageNotAutoSettledAfterRemtoeSettles(true);
+    }
+
+    private void doTestSentMessageNotAutoSettledAfterRemtoeSettles(boolean trySend) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(10)
+                             .withIncomingWindow(1024)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(1).queue();
+            peer.expectAttach().ofReceiver().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue", new SenderOptions().autoSettle(false));
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            // This ensures that the flow to sender is processed before we try-send
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload()
+                                 .respond()
+                                 .withSettled(true).withState().accepted();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Message<String> message = Message.create("Hello World");
+
+            final Tracker tracker;
+            if (trySend) {
+                tracker = sender.trySend(message);
+            } else {
+                tracker = sender.send(message);
+            }
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().get(5, TimeUnit.SECONDS));
+            assertEquals(tracker.remoteState().getType(), DeliveryState.Type.ACCEPTED);
+            assertNull(tracker.state());
+            assertFalse(tracker.settled());
+
+            sender.closeAsync();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSenderSendingSettledCompletesTrackerAcknowledgeFuture() throws Exception {
+        doTestSenderSendingSettledCompletesTrackerAcknowledgeFuture(false);
+    }
+
+    @Test
+    public void testSenderTrySendingSettledCompletesTrackerAcknowledgeFuture() throws Exception {
+        doTestSenderSendingSettledCompletesTrackerAcknowledgeFuture(true);
+    }
+
+    private void doTestSenderSendingSettledCompletesTrackerAcknowledgeFuture(boolean trySend) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender()
+                               .withSenderSettleModeSettled()
+                               .withReceivervSettlesFirst()
+                               .respond()
+                               .withSenderSettleModeSettled()
+                               .withReceivervSettlesFirst();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender sender = session.openSender("test-qos", options);
+            assertEquals("test-qos", sender.address());
+            session.openReceiver("dummy").openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+            final Tracker tracker;
+            if (trySend) {
+                tracker = sender.trySend(message);
+            } else {
+                tracker = sender.send(message);
+            }
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().isDone());
+            assertNotNull(tracker.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSenderIncrementsTransferTagOnEachSend() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_LEAST_ONCE).autoSettle(false);
+            Sender sender = session.openSender("test-tags", options).openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {0}).respond().withSettled(true).withState().accepted();
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {1}).respond().withSettled(true).withState().accepted();
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {2}).respond().withSettled(true).withState().accepted();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+            final Tracker tracker1 = sender.send(message);
+            final Tracker tracker2 = sender.send(message);
+            final Tracker tracker3 = sender.send(message);
+
+            assertNotNull(tracker1);
+            assertNotNull(tracker1.settlementFuture().get().settled());
+            assertNotNull(tracker2);
+            assertNotNull(tracker2.settlementFuture().get().settled());
+            assertNotNull(tracker3);
+            assertNotNull(tracker3.settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSenderSendsSettledInAtLeastOnceMode() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            Session session = connection.openSession().openFuture().get();
+            SenderOptions options = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE).autoSettle(false);
+            Sender sender = session.openSender("test-tags", options).openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {}).withSettled(true);
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {}).withSettled(true);
+            peer.expectTransfer().withNonNullPayload()
+                                 .withDeliveryTag(new byte[] {}).withSettled(true);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            final Message<String> message = Message.create("Hello World");
+            final Tracker tracker1 = sender.send(message);
+            final Tracker tracker2 = sender.send(message);
+            final Tracker tracker3 = sender.send(message);
+
+            assertNotNull(tracker1);
+            assertNotNull(tracker1.settlementFuture().get().settled());
+            assertNotNull(tracker2);
+            assertNotNull(tracker2.settlementFuture().get().settled());
+            assertNotNull(tracker3);
+            assertNotNull(tracker3.settlementFuture().get().settled());
+
+            sender.closeAsync().get();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateAnonymousSenderWhenRemoteDoesNotOfferSupportForIt() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.openAnonymousSender();
+                fail("Should not be able to open an anonymous sender when remote does not offer anonymous relay");
+            } catch (ClientUnsupportedOperationException unsupported) {
+                LOG.info("Caught expected error: ", unsupported);
+            }
+
+            connection.closeAsync();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateAnonymousSenderBeforeKnowingRemoteDoesNotOfferSupportForIt() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender anonymousSender = session.openAnonymousSender();
+            Message<String> message = Message.create("Hello World").to("my-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.remoteOpen().now();
+            peer.respondToLastBegin().now();
+            peer.expectClose().respond();
+
+            try {
+                anonymousSender.send(message);
+                fail("Should not be able to open an anonymous sender when remote does not offer anonymous relay");
+            } catch (ClientUnsupportedOperationException unsupported) {
+                LOG.info("Caught expected error: ", unsupported);
+            }
+
+            connection.closeAsync();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateAnonymousSenderAppliesOptions() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond().withOfferedCapabilities("ANONYMOUS-RELAY");
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withSenderSettleModeSettled()
+                                          .withReceivervSettlesFirst()
+                                          .withTarget().withAddress(Matchers.nullValue()).and().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            SenderOptions senderOptions = new SenderOptions().deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            Sender anonymousSender = session.openAnonymousSender(senderOptions);
+
+            anonymousSender.openFuture().get();
+
+            connection.closeAsync();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAnonymousSenderOpenHeldUntilConnectionOpenedAndSupportConfirmed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openAnonymousSender();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            // This should happen after we inject the held open and attach
+            peer.expectAttach().ofSender().withTarget().withAddress(Matchers.nullValue()).and().respond();
+            peer.expectClose().respond();
+
+            // Inject held responses to get the ball rolling again
+            peer.remoteOpen().withOfferedCapabilities("ANONYMOUS-RELAY").now();
+            peer.respondToLastBegin().now();
+
+            try {
+                sender.openFuture().get();
+            } catch (ExecutionException ex) {
+                fail("Open of Sender failed waiting for response: " + ex.getCause());
+            }
+
+            connection.closeAsync();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSenderGetRemotePropertiesWaitsForRemoteAttach() throws Exception {
+        tryReadSenderRemoteProperties(true);
+    }
+
+    @Test
+    public void testSenderGetRemotePropertiesFailsAfterOpenTimeout() throws Exception {
+        tryReadSenderRemoteProperties(false);
+    }
+
+    private void tryReadSenderRemoteProperties(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            SenderOptions options = new SenderOptions().openTimeout(75);
+            Sender sender = session.openSender("test-sender", options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Map<String, Object> expectedProperties = new HashMap<>();
+            expectedProperties.put("TEST", "test-property");
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withPropertiesMap(expectedProperties).later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(sender.properties(), "Remote should have responded with a remote properties value");
+                assertEquals(expectedProperties, sender.properties());
+            } else {
+                try {
+                    sender.properties();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                sender.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close when connection not closed and detach sent.");
+            }
+
+            LOG.debug("*** Test read remote properties ***");
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testGetRemoteOfferedCapabilitiesWaitsForRemoteAttach() throws Exception {
+        tryReadRemoteOfferedCapabilities(true);
+    }
+
+    @Test
+    public void testGetRemoteOfferedCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadRemoteOfferedCapabilities(false);
+    }
+
+    private void tryReadRemoteOfferedCapabilities(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(75);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Sender sender = session.openSender("test-sender");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withOfferedCapabilities("QUEUE").later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(sender.offeredCapabilities(), "Remote should have responded with a remote offered Capabilities value");
+                assertEquals(1, sender.offeredCapabilities().length);
+                assertEquals("QUEUE", sender.offeredCapabilities()[0]);
+            } else {
+                try {
+                    sender.offeredCapabilities();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                sender.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close when connection not closed and detach sent.");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testGetRemoteDesiredCapabilitiesWaitsForRemoteAttach() throws Exception {
+        tryReadRemoteDesiredCapabilities(true);
+    }
+
+    @Test
+    public void testGetRemoteDesiredCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadRemoteDesiredCapabilities(false);
+    }
+
+    private void tryReadRemoteDesiredCapabilities(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(75);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+            session.openFuture().get();
+
+            Sender sender = session.openSender("test-sender");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.respondToLastAttach().withDesiredCapabilities("Error-Free").later(10);
+            } else {
+                peer.expectDetach();
+            }
+
+            if (attachResponse) {
+                assertNotNull(sender.desiredCapabilities(), "Remote should have responded with a remote desired Capabilities value");
+                assertEquals(1, sender.desiredCapabilities().length);
+                assertEquals("Error-Free", sender.desiredCapabilities()[0]);
+            } else {
+                try {
+                    sender.desiredCapabilities();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                sender.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close when connection not closed and detach sent.");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenSenderWithLinCapabilities() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue())
+                               .withTarget().withCapabilities("queue").and()
+                               .respond();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Receiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get(10, TimeUnit.SECONDS);
+            SenderOptions senderOptions = new SenderOptions();
+            senderOptions.targetOptions().capabilities("queue");
+            Sender sender = session.openSender("test-queue", senderOptions);
+
+            sender.openFuture().get();
+            sender.close();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCloseSenderWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(true);
+    }
+
+    @Test
+    public void testDetachSenderWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(false);
+    }
+
+    public void doTestCloseOrDetachWithErrorCondition(boolean close) throws Exception {
+        final String condition = "amqp:link:detach-forced";
+        final String description = "something bad happened.";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.expectDetach().withClosed(close).withError(condition, description).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-sender");
+            sender.openFuture().get();
+
+            if (close) {
+                sender.closeAsync(ErrorCondition.create(condition, description, null));
+            } else {
+                sender.detachAsync(ErrorCondition.create(condition, description, null));
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMultipleMessages() throws Exception {
+        final int CREDIT = 20;
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withDeliveryCount(0).withLinkCredit(CREDIT).queue();
+            peer.expectAttach().ofReceiver().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get();
+
+            // This ensures that the flow to sender is processed before we try-send
+            Receiver receiver = session.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            final List<Tracker> sentMessages = new ArrayList<>();
+
+            for (int i = 0; i < CREDIT; ++i) {
+                peer.expectTransfer().withDeliveryId(i)
+                                     .withNonNullPayload()
+                                     .withSettled(false)
+                                     .respond()
+                                     .withSettled(true)
+                                     .withState().accepted();
+            }
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            Message<String> message = Message.create("Hello World");
+
+            for (int i = 0; i < CREDIT; ++i) {
+                final Tracker tracker = sender.send(message);
+                sentMessages.add(tracker);
+                tracker.settlementFuture().get();
+            }
+            assertEquals(CREDIT, sentMessages.size());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendBlockedForCreditFailsWhenLinkRemotelyClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteDetach().withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Link was deleted").afterDelay(25).queue();
+            peer.expectDetach();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get();
+
+            Message<String> message = Message.create("Hello World");
+
+            try {
+                sender.send(message);
+                fail("Send should have timed out.");
+            } catch (ClientResourceRemotelyClosedException cliEx) {
+                // Expected send to throw indicating that the remote closed the link
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendBlockedForCreditFailsWhenSessionRemotelyClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteEnd().withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Session was deleted").afterDelay(25).queue();
+            peer.expectEnd();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get();
+
+            Message<String> message = Message.create("Hello World");
+
+            try {
+                sender.send(message);
+                fail("Send should have timed out.");
+            } catch (ClientResourceRemotelyClosedException cliEx) {
+                // Expected send to throw indicating that the remote closed the session
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendBlockedForCreditFailsWhenConnectionRemotelyClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteClose().withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Connection was deleted").afterDelay(25).queue();
+            peer.expectClose();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get();
+
+            Message<String> message = Message.create("Hello World");
+
+            try {
+                sender.send(message);
+                fail("Send should have failed when Connection remotely closed.");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected send to throw indicating that the remote closed the connection
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendBlockedForCreditFailsWhenConnectionDrops() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.dropAfterLastHandler(25);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+            sender.openFuture().get();
+
+            Message<String> message = Message.create("Hello World");
+
+            try {
+                sender.send(message);
+                fail("Send should have timed out.");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected send to throw indicating that the remote closed unexpectedly
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendAfterConnectionDropsThrowsConnectionRemotelyClosedError() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+            peer.dropAfterLastHandler(25);
+            peer.start();
+
+            final CountDownLatch dropped = new CountDownLatch(1);
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.disconnectedHandler((connection, event) -> {
+                dropped.countDown();
+            });
+
+            URI remoteURI = peer.getServerURI();
+
+            Message<String> message = Message.create("test-message");
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test");
+
+            assertTrue(dropped.await(10, TimeUnit.SECONDS));
+
+            try {
+                sender.send(message);
+                fail("Send should fail with remotely closed error after remote drops");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected
+            }
+
+            try {
+                sender.trySend(message);
+                fail("trySend should fail with remotely closed error after remote drops");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAwaitSettlementFutureFailedAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectTransfer();
+            peer.dropAfterLastHandler();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Message<String> message = Message.create("test-message");
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test");
+
+            Tracker tracker = null;
+            try {
+                tracker = sender.send(message);
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                fail("Send not should fail with remotely closed error after remote drops");
+            }
+
+            // Connection should be dropped at this point and next call should test that after
+            // the drop the future has been completed
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            try {
+                tracker.settlementFuture().get();
+                fail("Wait for settlement should fail with remotely closed error after remote drops");
+            } catch (ExecutionException exe) {
+                assertTrue(exe.getCause() instanceof ClientConnectionRemotelyClosedException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAwaitSettlementFailedOnConnectionDropped() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectTransfer();
+            peer.dropAfterLastHandler(30);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Message<String> message = Message.create("test-message");
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test");
+
+            Tracker tracker = null;
+            try {
+                tracker = sender.send(message);
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                fail("Send should not fail with remotely closed error after remote drops");
+            }
+
+            // Most of the time this should await before connection drops testing that
+            // the drop completes waiting callers.
+            try {
+                tracker.awaitSettlement();
+                fail("Wait for settlement should fail with remotely closed error after remote drops");
+            } catch (ClientConnectionRemotelyClosedException cliRCEx) {
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBlockedSendThrowsConnectionRemotelyClosedError() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withTarget().withAddress("test").and().respond();
+            peer.dropAfterLastHandler(25);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Message<String> message = Message.create("test-message");
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test");
+
+            try {
+                sender.send(message);
+                fail("Send should fail with remotely closed error after remote drops");
+            } catch (ClientConnectionRemotelyClosedException cliEx) {
+                // Expected
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testAutoFlushDuringWriteThatExceedConfiguredBufferLimitSessionCreditLimitOnTransfer() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().withNextOutgoingId(0).respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().maxFrameSize(1024);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Sender sender = connection.openSender("test-queue");
+
+            final byte[] payload = new byte[4800];
+            Arrays.fill(payload, (byte) 1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("send failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(1).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).accept();
+
+            // Grant the credit to start meeting the above expectations
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(0).withLinkCredit(10).now();
+
+            peer.waitForScriptToComplete(500, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testAutoFlushDuringWriteWithRollingIncomingWindowUpdates() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().withNextOutgoingId(0).respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().maxFrameSize(1024);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Sender sender = connection.openSender("test-queue");
+
+            final byte[] payload = new byte[4800];
+            Arrays.fill(payload, (byte) 1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("send failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            // Credit should will be refilling as transfers arrive vs being exhausted on each
+            // incoming transfer and the send awaiting more credit.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.expectTransfer().withNonNullPayload().withMore(false).accept();
+
+            // Grant the credit to start meeting the above expectations
+            peer.remoteFlow().withIncomingWindow(2).withNextIncomingId(0).withLinkCredit(10).now();
+
+            peer.waitForScriptToComplete(500, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testConcurrentSendOnlyBlocksForInitialSendInProgress() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Sender sender = connection.openSender("test-queue").openFuture().get();
+            // Ensure that sender gets its flow before the sends are triggered.
+            connection.openReceiver("test-queue").openFuture().get();
+
+            final byte[] payload = new byte[1024];
+            Arrays.fill(payload, (byte) 1);
+
+            // One should block on the send waiting for the others send to finish
+            // otherwise they should not care about concurrency of sends.
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 2 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                } catch (Exception e) {
+                    LOG.info("Test send 2 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testConcurrentSendBlocksBehindSendWaitingForCredit() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Sender sender = connection.openSender("test-queue").openFuture().get();
+
+            final byte[] payload = new byte[1024];
+            Arrays.fill(payload, (byte) 1);
+
+            final CountDownLatch send1Started = new CountDownLatch(1);
+            final CountDownLatch send2Completed = new CountDownLatch(1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    ForkJoinPool.commonPool().execute(() -> send1Started.countDown());
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    assertTrue(send1Started.await(10, TimeUnit.SECONDS));
+                    LOG.info("Test send 2 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                    send2Completed.countDown();
+                } catch (Exception e) {
+                    LOG.info("Test send 2 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(0).withNextIncomingId(1).withLinkCredit(1).now();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(1).withNextIncomingId(2).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+
+            assertTrue(send2Completed.await(10, TimeUnit.SECONDS));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testConcurrentSendWaitingOnSplitFramedSendToCompleteIsSentAfterCreditUpdated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().maxFrameSize(1024);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Sender sender = connection.openSender("test-queue");
+
+            final byte[] payload = new byte[1536];
+            Arrays.fill(payload, (byte) 1);
+
+            final CountDownLatch send1Started = new CountDownLatch(1);
+            final CountDownLatch send2Completed = new CountDownLatch(1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    ForkJoinPool.commonPool().execute(() -> send1Started.countDown());
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    assertTrue(send1Started.await(10, TimeUnit.SECONDS));
+                    LOG.info("Test send 2 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                    send2Completed.countDown();
+                } catch (Exception e) {
+                    LOG.info("Test send 2 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(0).withNextIncomingId(1).withLinkCredit(1).now();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(0).withNextIncomingId(2).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(1).withNextIncomingId(3).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(1).withNextIncomingId(4).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+
+            assertTrue(send2Completed.await(10, TimeUnit.SECONDS));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateSenderWithDefaultSourceAndTargetOptions() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender()
+                               .withSource().withAddress(notNullValue())
+                                            .withDistributionMode(nullValue())
+                                            .withDefaultTimeout()
+                                            .withDurable(TerminusDurability.NONE)
+                                            .withExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH)
+                                            .withDefaultOutcome(nullValue())
+                                            .withCapabilities(nullValue())
+                                            .withFilter(nullValue())
+                                            .withOutcomes("amqp:accepted:list", "amqp:rejected:list", "amqp:released:list", "amqp:modified:list")
+                                            .also()
+                               .withTarget().withAddress("test-queue")
+                                            .withCapabilities(nullValue())
+                                            .withDurable(nullValue())
+                                            .withExpiryPolicy(nullValue())
+                                            .withDefaultTimeout()
+                                            .withDynamic(anyOf(nullValue(), equalTo(false)))
+                                            .withDynamicNodeProperties(nullValue())
+                               .and().respond();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue").openFuture().get();
+
+            sender.close();
+            session.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateSenderWithUserConfiguredSourceAndTargetOptions() throws Exception {
+        final Map<String, Object> filtersToObject = new HashMap<>();
+        filtersToObject.put("x-opt-filter", "a = b");
+
+        final Map<String, String> filters = new HashMap<>();
+        filters.put("x-opt-filter", "a = b");
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender()
+                               .withSource().withAddress(notNullValue())
+                                            .withDistributionMode("copy")
+                                            .withTimeout(128)
+                                            .withDurable(TerminusDurability.UNSETTLED_STATE)
+                                            .withExpiryPolicy(TerminusExpiryPolicy.CONNECTION_CLOSE)
+                                            .withDefaultOutcome(new Released())
+                                            .withCapabilities("QUEUE")
+                                            .withFilter(filtersToObject)
+                                            .withOutcomes("amqp:accepted:list", "amqp:rejected:list")
+                                            .also()
+                               .withTarget().withAddress("test-queue")
+                                            .withCapabilities("QUEUE")
+                                            .withDurable(TerminusDurability.CONFIGURATION)
+                                            .withExpiryPolicy(TerminusExpiryPolicy.SESSION_END)
+                                            .withTimeout(42)
+                                            .withDynamic(anyOf(nullValue(), equalTo(false)))
+                                            .withDynamicNodeProperties(nullValue())
+                               .and().respond();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            SenderOptions senderOptions = new SenderOptions();
+
+            senderOptions.sourceOptions().capabilities("QUEUE");
+            senderOptions.sourceOptions().distributionMode(DistributionMode.COPY);
+            senderOptions.sourceOptions().timeout(128);
+            senderOptions.sourceOptions().durabilityMode(DurabilityMode.UNSETTLED_STATE);
+            senderOptions.sourceOptions().expiryPolicy(ExpiryPolicy.CONNECTION_CLOSE);
+            senderOptions.sourceOptions().defaultOutcome(DeliveryState.released());
+            senderOptions.sourceOptions().filters(filters);
+            senderOptions.sourceOptions().outcomes(DeliveryState.Type.ACCEPTED, DeliveryState.Type.REJECTED);
+
+            senderOptions.targetOptions().capabilities("QUEUE");
+            senderOptions.targetOptions().durabilityMode(DurabilityMode.CONFIGURATION);
+            senderOptions.targetOptions().expiryPolicy(ExpiryPolicy.SESSION_CLOSE);
+            senderOptions.targetOptions().timeout(42);
+
+            Sender sender = session.openSender("test-queue", senderOptions).openFuture().get();
+
+            sender.close();
+            session.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testWaitForAcceptedReturnsOnRemoteAcceptance() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Sender sender = connection.openSender("test-queue").openFuture().get();
+            Tracker tracker = sender.send(Message.create("Hello World"));
+            tracker.awaitAccepted();
+
+            assertTrue(tracker.remoteSettled());
+            assertTrue(tracker.remoteState().isAccepted());
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testWaitForAcceptanceFailsIfRemoteSendsRejceted() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().rejected();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Sender sender = connection.openSender("test-queue").openFuture().get();
+            Tracker tracker = sender.send(Message.create("Hello World"));
+
+            try {
+                tracker.awaitAccepted(10, TimeUnit.SECONDS);
+                fail("Should not succeed since remote sent something other than Accepted");
+            } catch (ClientDeliveryStateException dlvEx) {
+                // Expected
+            }
+
+            assertTrue(tracker.remoteSettled());
+            assertFalse(tracker.remoteState().isAccepted());
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testWaitForAcceptanceFailsIfRemoteSendsNoDisposition() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Sender sender = connection.openSender("test-queue").openFuture().get();
+            Tracker tracker = sender.send(Message.create("Hello World"));
+
+            try {
+                tracker.awaitAccepted(10, TimeUnit.SECONDS);
+                fail("Should not succeed since remote sent something other than Accepted");
+            } catch (ClientDeliveryStateException dlvEx) {
+                // Expected
+            }
+
+            assertTrue(tracker.remoteSettled());
+            assertNull(tracker.remoteState());
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSenderLinkNameOptionAppliedWhenSet() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().withName("custom-link-name").respond();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            SenderOptions senderOptions = new SenderOptions().linkName("custom-link-name");
+            Sender sender = session.openSender("test-queue", senderOptions);
+
+            sender.openFuture().get();
+            sender.close();
+
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testInspectRemoteSourceMatchesValuesSent() throws Exception {
+        Map<String, Object> remoteFilters = new HashMap<>();
+        remoteFilters.put("filter-1", "value1");
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond().withSource().withOutcomes("Accepted", "Released")
+                                                                 .withCapabilities("Queue")
+                                                                 .withDistributionMode("COPY")
+                                                                 .withDynamic(false)
+                                                                 .withExpiryPolicy(TerminusExpiryPolicy.SESSION_END)
+                                                                 .withDurability(TerminusDurability.UNSETTLED_STATE)
+                                                                 .withDefaultOutcome(Released.getInstance())
+                                                                 .withTimeout(Integer.MAX_VALUE)
+                                                                 .withFilterMap(remoteFilters)
+                                                                 .withAddress("test-queue");
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+
+            Source remoteSource = sender.source();
+
+            assertTrue(remoteSource.outcomes().contains(DeliveryState.Type.ACCEPTED));
+            assertTrue(remoteSource.capabilities().contains("Queue"));
+            assertEquals("test-queue", remoteSource.address());
+            assertFalse(remoteSource.dynamic());
+            assertNull(remoteSource.dynamicNodeProperties());
+            assertEquals(DistributionMode.COPY, remoteSource.distributionMode());
+            assertEquals(DeliveryState.released(), remoteSource.defaultOutcome());
+            assertEquals(Integer.MAX_VALUE, remoteSource.timeout());
+            assertEquals(DurabilityMode.UNSETTLED_STATE, remoteSource.durabilityMode());
+            assertEquals(ExpiryPolicy.SESSION_CLOSE, remoteSource.expiryPolicy());
+            assertEquals(remoteFilters, remoteSource.filters());
+
+            sender.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testInspectRemoteTargetMatchesValuesSent() throws Exception {
+        Map<String, Object> remoteFilters = new HashMap<>();
+        remoteFilters.put("filter-1", "value1");
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond().withTarget().withCapabilities("Queue")
+                                                                 .withDynamic(false)
+                                                                 .withExpiryPolicy(TerminusExpiryPolicy.SESSION_END)
+                                                                 .withDurability(TerminusDurability.UNSETTLED_STATE)
+                                                                 .withTimeout(Integer.MAX_VALUE)
+                                                                 .withAddress("test-queue");
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Sender sender = session.openSender("test-queue");
+
+            Target remoteTarget = sender.target();
+
+            assertTrue(remoteTarget.capabilities().contains("Queue"));
+            assertEquals("test-queue", remoteTarget.address());
+            assertFalse(remoteTarget.dynamic());
+            assertEquals(Integer.MAX_VALUE, remoteTarget.timeout());
+            assertEquals(DurabilityMode.UNSETTLED_STATE, remoteTarget.durabilityMode());
+            assertEquals(ExpiryPolicy.SESSION_CLOSE, remoteTarget.expiryPolicy());
+
+            sender.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SessionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SessionTest.java
new file mode 100644
index 0000000..ae5b74b
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SessionTest.java
@@ -0,0 +1,592 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.SessionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test behaviors of the Session API
+ */
+@Timeout(20)
+public class SessionTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SessionTest.class);
+
+    @Test
+    public void testSessionOpenTimeoutWhenNoRemoteBeginArrivesTimeout() throws Exception {
+        doTestSessionOpenTimeoutWhenNoRemoteBeginArrives(true);
+    }
+
+    @Test
+    public void testSessionOpenTimeoutWhenNoRemoteBeginArrivesNoTimeout() throws Exception {
+        doTestSessionOpenTimeoutWhenNoRemoteBeginArrives(false);
+    }
+
+    private void doTestSessionOpenTimeoutWhenNoRemoteBeginArrives(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.expectEnd();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            SessionOptions options = new SessionOptions();
+            options.openTimeout(75);
+            Session session = connection.openSession(options);
+
+            try {
+                if (timeout) {
+                    session.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    session.openFuture().get();
+                }
+
+                fail("Session Open should timeout when no Begin response and complete future with error.");
+            } catch (Throwable error) {
+                LOG.info("Session open failed with error: ", error);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionOpenWaitWithTimeoutCanceledWhenConnectionDrops() throws Exception {
+        doTestSessionOpenWaitCanceledWhenConnectionDrops(true);
+    }
+
+    @Test
+    public void testSessionOpenWaitWithNoTimeoutCanceledWhenConnectionDrops() throws Exception {
+        doTestSessionOpenWaitCanceledWhenConnectionDrops(false);
+    }
+
+    private void doTestSessionOpenWaitCanceledWhenConnectionDrops(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+
+            try {
+                if (timeout) {
+                    session.openFuture().get(10, TimeUnit.SECONDS);
+                } else {
+                    session.openFuture().get();
+                }
+
+                fail("Session Open should wait should abort when connection drops.");
+            } catch (ExecutionException error) {
+                LOG.info("Session open failed with error: ", error);
+                assertTrue(error.getCause() instanceof ClientIOException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionCloseTimeoutWhenNoRemoteEndArrivesTimeout() throws Exception {
+        doTestSessionCloseTimeoutWhenNoRemoteEndArrives(true);
+    }
+
+    @Test
+    public void testSessionCloseTimeoutWhenNoRemoteEndArrivesNoTimeout() throws Exception {
+        doTestSessionCloseTimeoutWhenNoRemoteEndArrives(false);
+    }
+
+    private void doTestSessionCloseTimeoutWhenNoRemoteEndArrives(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectEnd();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            SessionOptions options = new SessionOptions();
+            options.closeTimeout(75);
+            Session session = connection.openSession(options).openFuture().get();
+
+            try {
+                if (timeout) {
+                    session.closeAsync().get(10, TimeUnit.SECONDS);
+                } else {
+                    session.closeAsync().get();
+                }
+
+                fail("Close should throw an error if the Session end doesn't arrive in time");
+            } catch (Throwable error) {
+                LOG.info("Session close failed with error: ", error);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionCloseWaitWithTimeoutCanceledWhenConnectionDrops() throws Exception {
+        doTestSessionCloseWaitCanceledWhenConnectionDrops(true);
+    }
+
+    @Test
+    public void testSessionCloseWaitWithNoTimeoutCanceledWhenConnectionDrops() throws Exception {
+        doTestSessionCloseWaitCanceledWhenConnectionDrops(false);
+    }
+
+    private void doTestSessionCloseWaitCanceledWhenConnectionDrops(boolean timeout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectEnd();
+            peer.dropAfterLastHandler(10);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            SessionOptions options = new SessionOptions();
+            options.closeTimeout(75);
+            Session session = connection.openSession(options).openFuture().get();
+
+            try {
+                if (timeout) {
+                    session.closeAsync().get(10, TimeUnit.SECONDS);
+                } else {
+                    session.closeAsync().get();
+                }
+            } catch (ExecutionException error) {
+                fail("Session Close should complete when parent connection drops.");
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionCloseGetsResponseWithErrorDoesNotThrowTimedGet() throws Exception {
+        doTestSessionCloseGetsResponseWithErrorThrows(true);
+    }
+
+    @Test
+    public void testConnectionCloseGetsResponseWithErrorDoesNotThrowUntimedGet() throws Exception {
+        doTestSessionCloseGetsResponseWithErrorThrows(false);
+    }
+
+    protected void doTestSessionCloseGetsResponseWithErrorThrows(boolean tiemout) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectEnd().respond().withErrorCondition(AmqpError.INTERNAL_ERROR.toString(), "Something odd happened.");
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            if (tiemout) {
+                // Should close normally and not throw error as we initiated the close.
+                session.closeAsync().get(10, TimeUnit.SECONDS);
+            } else {
+                // Should close normally and not throw error as we initiated the close.
+                session.closeAsync().get();
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionGetRemotePropertiesWaitsForRemoteBegin() throws Exception {
+        tryReadSessionRemoteProperties(true);
+    }
+
+    @Test
+    public void testSessionGetRemotePropertiesFailsAfterOpenTimeout() throws Exception {
+        tryReadSessionRemoteProperties(false);
+    }
+
+    private void tryReadSessionRemoteProperties(boolean beginResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Map<String, Object> expectedProperties = new HashMap<>();
+            expectedProperties.put("TEST", "test-property");
+
+            if (beginResponse) {
+                peer.expectEnd().respond();
+                peer.respondToLastBegin().withProperties(expectedProperties).later(10);
+            } else {
+                peer.expectEnd();
+            }
+
+            if (beginResponse) {
+                assertNotNull(session.properties(), "Remote should have responded with a remote properties value");
+                assertEquals(expectedProperties, session.properties());
+            } else {
+                try {
+                    session.properties();
+                    fail("Should failed to get remote state due to no begin response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                session.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close when connection not closed and end was sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionGetRemoteOfferedCapabilitiesWaitsForRemoteBegin() throws Exception {
+        tryReadSessionRemoteOfferedCapabilities(true);
+    }
+
+    @Test
+    public void testSessionGetRemoteOfferedCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadSessionRemoteOfferedCapabilities(false);
+    }
+
+    private void tryReadSessionRemoteOfferedCapabilities(boolean beginResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            Session session = connection.openSession();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (beginResponse) {
+                peer.expectEnd().respond();
+                peer.respondToLastBegin().withOfferedCapabilities("transactions").later(10);
+            } else {
+                peer.expectEnd();
+            }
+
+            if (beginResponse) {
+                assertNotNull(session.offeredCapabilities(), "Remote should have responded with a remote offered Capabilities value");
+                assertEquals(1, session.offeredCapabilities().length);
+                assertEquals("transactions", session.offeredCapabilities()[0]);
+            } else {
+                try {
+                    session.offeredCapabilities();
+                    fail("Should failed to get remote state due to no begin response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                session.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close when connection not closed and end was sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionGetRemoteDesiredCapabilitiesWaitsForRemoteBegin() throws Exception {
+        tryReadSessionRemoteDesiredCapabilities(true);
+    }
+
+    @Test
+    public void testSessionGetRemoteDesiredCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadSessionRemoteDesiredCapabilities(false);
+    }
+
+    private void tryReadSessionRemoteDesiredCapabilities(boolean beginResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (beginResponse) {
+                peer.expectEnd().respond();
+                peer.respondToLastBegin().withDesiredCapabilities("Error-Free").later(10);
+            } else {
+                peer.expectEnd();
+            }
+
+            if (beginResponse) {
+                assertNotNull(session.desiredCapabilities(), "Remote should have responded with a remote desired Capabilities value");
+                assertEquals(1, session.desiredCapabilities().length);
+                assertEquals("Error-Free", session.desiredCapabilities()[0]);
+            } else {
+                try {
+                    session.desiredCapabilities();
+                    fail("Should failed to get remote state due to no begin response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                session.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close to when connection not closed and end sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testQuickOpenCloseWhenNoBeginResponseFailsFastOnOpenTimeout() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin();
+            peer.expectEnd();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            ConnectionOptions options = new ConnectionOptions();
+            options.openTimeout(100);
+            options.closeTimeout(TimeUnit.HOURS.toMillis(1));  // Test would timeout if waited on.
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            connection.openFuture().get();
+
+            try {
+                connection.openSession().closeAsync().get();
+            } catch (ExecutionException error) {
+                fail("Should not fail when waiting on close with quick open timeout");
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCloseWithErrorConditionSync() throws Exception {
+        doTestCloseWithErrorCondition(true);
+    }
+
+    @Test
+    public void testCloseWithErrorConditionAsync() throws Exception {
+        doTestCloseWithErrorCondition(false);
+    }
+
+    private void doTestCloseWithErrorCondition(boolean sync) throws Exception {
+        final String condition = "amqp:precondition-failed";
+        final String description = "something bad happened.";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectEnd().withError(condition, description).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+
+            session.openFuture().get();
+
+            assertEquals(session.client(), container);
+
+            if (sync) {
+                session.close(ErrorCondition.create(condition, description, null));
+            } else {
+                session.closeAsync(ErrorCondition.create(condition, description, null));
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotCreateResourcesFromClosedSession() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+
+            session.openFuture().get();
+            session.close();
+
+            assertThrows(ClientIllegalStateException.class, () -> session.openReceiver("test"));
+            assertThrows(ClientIllegalStateException.class, () -> session.openReceiver("test", new ReceiverOptions()));
+            assertThrows(ClientIllegalStateException.class, () -> session.openDurableReceiver("test", "test"));
+            assertThrows(ClientIllegalStateException.class, () -> session.openDurableReceiver("test", "test", new ReceiverOptions()));
+            assertThrows(ClientIllegalStateException.class, () -> session.openDynamicReceiver());
+            assertThrows(ClientIllegalStateException.class, () -> session.openDynamicReceiver(new HashMap<>()));
+            assertThrows(ClientIllegalStateException.class, () -> session.openDynamicReceiver(new ReceiverOptions()));
+            assertThrows(ClientIllegalStateException.class, () -> session.openDynamicReceiver(new HashMap<>(), new ReceiverOptions()));
+            assertThrows(ClientIllegalStateException.class, () -> session.openSender("test"));
+            assertThrows(ClientIllegalStateException.class, () -> session.openSender("test", new SenderOptions()));
+            assertThrows(ClientIllegalStateException.class, () -> session.openAnonymousSender());
+            assertThrows(ClientIllegalStateException.class, () -> session.openAnonymousSender(new SenderOptions()));
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java
new file mode 100644
index 0000000..8b71151
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java
@@ -0,0 +1,612 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.client.transport.SslSupport;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+import org.apache.qpid.protonj2.types.security.SaslCode;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.handler.ssl.OpenSsl;
+
+/**
+ * Test for the Connection class
+ */
+@Timeout(30)
+public class SslConnectionTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SslConnectionTest.class);
+
+    private static final String BROKER_JKS_KEYSTORE = "src/test/resources/broker-jks.keystore";
+    private static final String BROKER_PKCS12_KEYSTORE = "src/test/resources/broker-pkcs12.keystore";
+    private static final String BROKER_JKS_TRUSTSTORE = "src/test/resources/broker-jks.truststore";
+    private static final String BROKER_PKCS12_TRUSTSTORE = "src/test/resources/broker-pkcs12.truststore";
+    private static final String CLIENT_MULTI_KEYSTORE = "src/test/resources/client-multiple-keys-jks.keystore";
+    private static final String CLIENT_JKS_TRUSTSTORE = "src/test/resources/client-jks.truststore";
+    private static final String CLIENT_PKCS12_TRUSTSTORE = "src/test/resources/client-pkcs12.truststore";
+    private static final String OTHER_CA_TRUSTSTORE = "src/test/resources/other-ca-jks.truststore";
+    private static final String CLIENT_JKS_KEYSTORE = "src/test/resources/client-jks.keystore";
+    private static final String CLIENT_PKCS12_KEYSTORE = "src/test/resources/client-pkcs12.keystore";
+    private static final String CLIENT2_JKS_KEYSTORE = "src/test/resources/client2-jks.keystore";
+    private static final String CUSTOM_STORE_TYPE_PKCS12 = "pkcs12";
+    private static final String PASSWORD = "password";
+    private static final String WRONG_PASSWORD = "wrong-password";
+
+    private static final String CLIENT_KEY_ALIAS = "client";
+    private static final String CLIENT_DN = "O=Client,CN=client";
+    private static final String CLIENT2_KEY_ALIAS = "client2";
+    private static final String CLIENT2_DN = "O=Client2,CN=client2";
+
+    private static final String ALIAS_DOES_NOT_EXIST = "alias.does.not.exist";
+    private static final String ALIAS_CA_CERT = "ca";
+
+    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
+    private static final String JAVAX_NET_SSL_KEY_STORE_TYPE = "javax.net.ssl.keyStoreType";
+    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
+    private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword";
+
+    protected ProtonTestServerOptions serverOptions() {
+        return new ProtonTestServerOptions();
+    }
+
+    protected ConnectionOptions connectionOptions() {
+        return new ConnectionOptions().sslEnabled(true);
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionJDK() throws Exception {
+        testCreateAndCloseSslConnection(false);
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        testCreateAndCloseSslConnection(true);
+    }
+
+    private void testCreateAndCloseSslConnection(boolean openSSL) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD)
+                         .allowNativeSSL(openSSL);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertFalse(peer.isConnectionVerified());
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionWithDefaultPortJDK() throws Exception {
+        testCreateAndCloseSslConnectionWithDefaultPort(false);
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionWithDefaultPortOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        testCreateAndCloseSslConnectionWithDefaultPort(true);
+    }
+
+    private void testCreateAndCloseSslConnectionWithDefaultPort(boolean openSSL) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD)
+                         .allowNativeSSL(openSSL)
+                         .defaultSslPort(peer.getServerURI().getPort());
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertFalse(peer.isConnectionVerified());
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete();
+        }
+    }
+
+    @Disabled("Test driver not handling the preemptive header currently")
+    @Test
+    public void testCreateSslConnectionWithServerSendingPreemptiveDataJDK() throws Exception {
+        doTestCreateSslConnectionWithServerSendingPreemptiveData(false);
+    }
+
+    @Disabled("Test driver not handling the preemptive header currently")
+    @Test
+    public void testCreateSslConnectionWithServerSendingPreemptiveDataOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        doTestCreateSslConnectionWithServerSendingPreemptiveData(true);
+    }
+
+    private void doTestCreateSslConnectionWithServerSendingPreemptiveData(boolean openSSL) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+
+            peer.remoteHeader(AMQPHeader.getSASLHeader().toArray()).queue();
+            peer.expectSASLHeader();
+            peer.remoteSaslMechanisms().withMechanisms("ANONYMOUS").queue();
+            peer.expectSaslInit().withMechanism("ANONYMOUS");
+            peer.remoteSaslOutcome().withCode(SaslCode.OK.byteValue()).queue();
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD)
+                         .allowNativeSSL(openSSL);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionWithClientAuthJDK() throws Exception {
+        doTestCreateAndCloseSslConnectionWithClientAuth(false);
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionWithClientAuthOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        doTestCreateAndCloseSslConnectionWithClientAuth(true);
+    }
+
+    private void doTestCreateAndCloseSslConnectionWithClientAuth(boolean openSSL) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+        serverOptions.setTrustStorePassword(PASSWORD);
+        serverOptions.setNeedClientAuth(true);
+        serverOptions.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .keyStoreLocation(CLIENT_MULTI_KEYSTORE)
+                         .keyStorePassword(PASSWORD)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD)
+                         .allowNativeSSL(openSSL);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertTrue(peer.isConnectionVerified());
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionWithAliasJDK() throws Exception {
+        doConnectionWithAliasTestImpl(CLIENT_KEY_ALIAS, CLIENT_DN, false);
+        doConnectionWithAliasTestImpl(CLIENT2_KEY_ALIAS, CLIENT2_DN, false);
+    }
+
+    @Test
+    public void testCreateAndCloseSslConnectionWithAliasOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        doConnectionWithAliasTestImpl(CLIENT_KEY_ALIAS, CLIENT_DN, true);
+        doConnectionWithAliasTestImpl(CLIENT2_KEY_ALIAS, CLIENT2_DN, true);
+    }
+
+    private void doConnectionWithAliasTestImpl(String alias, String expectedDN, boolean requestOpenSSL) throws Exception, SSLPeerUnverifiedException, IOException {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setTrustStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+        serverOptions.setNeedClientAuth(true);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .keyStoreLocation(CLIENT_MULTI_KEYSTORE)
+                         .keyStorePassword(PASSWORD)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD)
+                         .keyAlias(alias)
+                         .allowNativeSSL(requestOpenSSL);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertTrue(peer.isConnectionVerified());
+
+            SSLSession session = peer.getConnectionSSLEngine().getSession();
+
+            Certificate[] peerCertificates = session.getPeerCertificates();
+            assertNotNull(peerCertificates);
+
+            Certificate cert = peerCertificates[0];
+            assertTrue(cert instanceof X509Certificate);
+            String dn = ((X509Certificate) cert).getSubjectX500Principal().getName();
+            assertEquals(expectedDN, dn, "Unexpected certificate DN");
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateConnectionWithAliasThatDoesNotExist() throws Exception {
+        doCreateConnectionWithInvalidAliasTestImpl(ALIAS_DOES_NOT_EXIST);
+    }
+
+    @Test
+    public void testCreateConnectionWithAliasThatDoesNotRepresentKeyEntry() throws Exception {
+        doCreateConnectionWithInvalidAliasTestImpl(ALIAS_CA_CERT);
+    }
+
+    private void doCreateConnectionWithInvalidAliasTestImpl(String alias) throws Exception, IOException {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setTrustStorePassword(PASSWORD);
+        serverOptions.setVerifyHost(false);
+        serverOptions.setNeedClientAuth(true);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.sslOptions()
+                         .keyStoreLocation(CLIENT_MULTI_KEYSTORE)
+                         .keyStorePassword(PASSWORD)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD)
+                         .keyAlias(alias);
+
+            Client container = Client.create();
+
+            try {
+                container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+                fail("Should have failed to connect using invalid alias");
+            } catch (Throwable clix) {
+                LOG.info("Client failed to open due to error: ", clix);
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            assertTrue(peer.isAcceptingConnections(), "Attempt should have failed locally, peer should not have accepted any TCP connection");
+        }
+    }
+
+    /**
+     * Checks that configuring different SSLContext instances using different client key
+     * stores via {@link SslOptions#sslContextOverride(javax.net.ssl.SSLContext)} results
+     * in different certificates being observed server side following handshake.
+     *
+     * @throws Exception if an unexpected error is encountered
+     */
+    @Test
+    public void testCreateConnectionWithSslContextOverride() throws Exception {
+        assertNotEquals(CLIENT_JKS_KEYSTORE, CLIENT2_JKS_KEYSTORE);
+        assertNotEquals(CLIENT_DN, CLIENT2_DN);
+
+        // Connect providing the Client 1 details via context override, expect Client1 DN.
+        doConnectionWithSslContextOverride(CLIENT_JKS_KEYSTORE, CLIENT_DN);
+        // Connect providing the Client 2 details via context override, expect Client2 DN instead.
+        doConnectionWithSslContextOverride(CLIENT2_JKS_KEYSTORE, CLIENT2_DN);
+    }
+
+    private void doConnectionWithSslContextOverride(String clientKeyStorePath, String expectedDN) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+        serverOptions.setKeyStorePassword(PASSWORD);
+        serverOptions.setTrustStorePassword(PASSWORD);
+        serverOptions.setNeedClientAuth(true);
+        serverOptions.setVerifyHost(false);
+
+        SslOptions clientSslOptions = new SslOptions();
+        clientSslOptions.sslEnabled(true)
+                        .keyStoreLocation(clientKeyStorePath)
+                        .keyStorePassword(PASSWORD)
+                        .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                        .trustStorePassword(PASSWORD);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSASLPlainConnect("guest", "guest");
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            SSLContext sslContext = SslSupport.createJdkSslContext(clientSslOptions);
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user("guest")
+                         .password("guest")
+                         .sslOptions()
+                         .sslContextOverride(sslContext);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertTrue(peer.isConnectionVerified());
+
+            SSLSession session = peer.getConnectionSSLEngine().getSession();
+
+            Certificate[] peerCertificates = session.getPeerCertificates();
+            assertNotNull(peerCertificates);
+
+            Certificate cert = peerCertificates[0];
+            assertTrue(cert instanceof X509Certificate);
+            String dn = ((X509Certificate) cert).getSubjectX500Principal().getName();
+            assertEquals(expectedDN, dn, "Unexpected certificate DN");
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConfigureStoresWithSslSystemProperties() throws Exception {
+        // Set properties and expect connection as Client1
+        setSslSystemPropertiesForCurrentTest(CLIENT_JKS_KEYSTORE, PASSWORD, CLIENT_JKS_TRUSTSTORE, PASSWORD);
+        doConfigureStoresWithSslSystemPropertiesTestImpl(CLIENT_DN);
+
+        // Set properties with 'wrong ca' trust store and expect connection to fail
+        setSslSystemPropertiesForCurrentTest(CLIENT_JKS_KEYSTORE, PASSWORD, OTHER_CA_TRUSTSTORE, PASSWORD);
+        try {
+            doConfigureStoresWithSslSystemPropertiesTestImpl(null);
+            fail("Connection should have failed due to wrong CA");
+        } catch (Throwable clix) {
+            // Expected
+        }
+
+        // Set properties with wrong key store password and expect connection to fail
+        setSslSystemPropertiesForCurrentTest(CLIENT_JKS_KEYSTORE, WRONG_PASSWORD, CLIENT_JKS_TRUSTSTORE, PASSWORD);
+        try {
+            doConfigureStoresWithSslSystemPropertiesTestImpl(null);
+            fail("Connection should have failed due to wrong keystore password");
+        } catch (Throwable jmse) {
+            // Expected
+        }
+
+        // Set properties with wrong trust store password and expect connection to fail
+        setSslSystemPropertiesForCurrentTest(CLIENT_JKS_KEYSTORE, PASSWORD, CLIENT_JKS_TRUSTSTORE, WRONG_PASSWORD);
+        try {
+            doConfigureStoresWithSslSystemPropertiesTestImpl(null);
+            fail("Connection should have failed due to wrong truststore password");
+        } catch (Throwable jmse) {
+            // Expected
+        }
+
+        // Set properties and expect connection as Client2
+        setSslSystemPropertiesForCurrentTest(CLIENT2_JKS_KEYSTORE, PASSWORD, CLIENT_JKS_TRUSTSTORE, PASSWORD);
+        doConfigureStoresWithSslSystemPropertiesTestImpl(CLIENT2_DN);
+    }
+
+    @Test
+    public void testConfigurePkcs12StoresWithSslSystemProperties() throws Exception {
+        // Set properties and expect connection as Client1
+        setSslSystemPropertiesForCurrentTest(CLIENT_PKCS12_KEYSTORE, CUSTOM_STORE_TYPE_PKCS12, PASSWORD, CLIENT_PKCS12_TRUSTSTORE, CUSTOM_STORE_TYPE_PKCS12, PASSWORD);
+        doConfigureStoresWithSslSystemPropertiesTestImpl(CLIENT_DN, true);
+    }
+
+    private void setSslSystemPropertiesForCurrentTest(String keystore, String keystorePassword, String truststore, String truststorePassword) {
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE, keystore);
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD, keystorePassword);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE, truststore);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD, truststorePassword);
+    }
+
+    private void setSslSystemPropertiesForCurrentTest(String keystore, String keystoreType, String keystorePassword, String truststore, String truststoreType, String truststorePassword) {
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE, keystore);
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE_TYPE, keystoreType);
+        setTestSystemProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD, keystorePassword);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE, truststore);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE_TYPE, truststoreType);
+        setTestSystemProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD, truststorePassword);
+    }
+
+    private void doConfigureStoresWithSslSystemPropertiesTestImpl(String expectedDN) throws Exception {
+        doConfigureStoresWithSslSystemPropertiesTestImpl(expectedDN, false);
+    }
+
+    private void doConfigureStoresWithSslSystemPropertiesTestImpl(String expectedDN, boolean usePkcs12Store) throws Exception {
+        ProtonTestServerOptions serverOptions = serverOptions();
+        serverOptions.setSecure(true);
+        serverOptions.setNeedClientAuth(true);
+
+        if (!usePkcs12Store) {
+            serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE);
+            serverOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE);
+            serverOptions.setKeyStorePassword(PASSWORD);
+            serverOptions.setTrustStorePassword(PASSWORD);
+            serverOptions.setVerifyHost(false);
+        } else {
+            serverOptions.setKeyStoreLocation(BROKER_PKCS12_KEYSTORE);
+            serverOptions.setTrustStoreLocation(BROKER_PKCS12_TRUSTSTORE);
+            serverOptions.setKeyStoreType(CUSTOM_STORE_TYPE_PKCS12);
+            serverOptions.setTrustStoreType(CUSTOM_STORE_TYPE_PKCS12);
+            serverOptions.setKeyStorePassword(PASSWORD);
+            serverOptions.setTrustStorePassword(PASSWORD);
+            serverOptions.setVerifyHost(false);
+        }
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            Client container = Client.create();
+            ConnectionOptions clientOptions = connectionOptions();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertTrue(peer.hasSecureConnection());
+            assertTrue(peer.isConnectionVerified());
+
+            SSLSession session = peer.getConnectionSSLEngine().getSession();
+
+            Certificate[] peerCertificates = session.getPeerCertificates();
+            assertNotNull(peerCertificates);
+
+            Certificate cert = peerCertificates[0];
+            assertTrue(cert instanceof X509Certificate);
+            String dn = ((X509Certificate)cert).getSubjectX500Principal().getName();
+            assertEquals(expectedDN, dn, "Unexpected certificate DN");
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/StreamReceiverTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/StreamReceiverTest.java
new file mode 100644
index 0000000..987fea4
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/StreamReceiverTest.java
@@ -0,0 +1,3763 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.ErrorCondition;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.StreamDelivery;
+import org.apache.qpid.protonj2.client.StreamReceiver;
+import org.apache.qpid.protonj2.client.StreamReceiverMessage;
+import org.apache.qpid.protonj2.client.StreamReceiverOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientDeliveryAbortedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientLinkRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientOperationTimedOutException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.client.test.Wait;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests the {@link ReceiveContext} implementation
+ */
+@Timeout(20)
+class StreamReceiverTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(StreamReceiverTest.class);
+
+    @Test
+    public void testCreateReceiverAndClose() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLink(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetach() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLink(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDetachLink(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            assertSame(container, receiver.client());
+            assertSame(connection, receiver.connection());
+
+            if (close) {
+                receiver.closeAsync().get(10, TimeUnit.SECONDS);
+            } else {
+                receiver.detachAsync().get(10, TimeUnit.SECONDS);
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverAndCloseSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachSyncLink(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetachSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachSyncLink(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDetachSyncLink(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(10);
+            peer.expectDetach().withClosed(close).respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                receiver.close();
+            } else {
+                receiver.detach();
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReceiverAndCloseWithErrorSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDeatchWithErrorSync(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetachWithErrorSync() throws Exception {
+        doTestCreateReceiverAndCloseOrDeatchWithErrorSync(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDeatchWithErrorSync(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectDetach().withError("amqp-resource-deleted", "an error message").withClosed(close).respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                receiver.close(ErrorCondition.create("amqp-resource-deleted", "an error message", null));
+            } else {
+                receiver.detach(ErrorCondition.create("amqp-resource-deleted", "an error message", null));
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateReveiverAndCloseWithErrorAsync() throws Exception {
+        doTestCreateReveiverAndCloseOrDeatchWithErrorAsync(true);
+    }
+
+    @Test
+    public void testCreateReveiverAndDetachWithErrorAsync() throws Exception {
+        doTestCreateReveiverAndCloseOrDeatchWithErrorAsync(false);
+    }
+
+    private void doTestCreateReveiverAndCloseOrDeatchWithErrorAsync(boolean close) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectDetach().withError("amqp-resource-deleted", "an error message").withClosed(close).respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            if (close) {
+                receiver.closeAsync(ErrorCondition.create("amqp-resource-deleted", "an error message", null)).get();
+            } else {
+                receiver.detachAsync(ErrorCondition.create("amqp-resource-deleted", "an error message", null)).get();
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReceiverConfiguresSessionCapacity_1() throws Exception {
+        // Read buffer is always halved by connection when creating new session for the stream
+        doTestStreamReceiverSessionCapacity(100_000, 200_000, 1);
+    }
+
+    @Test
+    public void testStreamReceiverConfiguresSessionCapacity_2() throws Exception {
+        // Read buffer is always halved by connection when creating new session for the stream
+        doTestStreamReceiverSessionCapacity(100_000, 400_000, 2);
+    }
+
+    @Test
+    public void testStreamReceiverConfiguresSessionCapacity_3() throws Exception {
+        // Read buffer is always halved by connection when creating new session for the stream
+        doTestStreamReceiverSessionCapacity(100_000, 600_000, 3);
+    }
+
+    @Test
+    public void testStreamReceiverConfiguresSessionCapacityIdenticalToMaxFrameSize() throws Exception {
+        // Read buffer is always halved by connection when creating new session for the stream
+        // unless it falls at the max frame size value which means only one is possible, in this
+        // case the user configured session window the same as than max frame size so only one
+        // frame is possible.
+        doTestStreamReceiverSessionCapacity(100_000, 100_000, 1);
+    }
+
+    @Test
+    public void testStreamReceiverConfiguresSessionCapacityLowerThanMaxFrameSize() throws Exception {
+        // Read buffer is always halved by connection when creating new session for the stream
+        // unless it falls at the max frame size value which means only one is possible, in this
+        // case the user configured session window lower than max frame size and the client auto
+        // adjusts that to one frame.
+        doTestStreamReceiverSessionCapacity(100_000, 50_000, 1);
+    }
+
+    private void doTestStreamReceiverSessionCapacity(int maxFrameSize, int readBufferSize, int expectedSessionWindow) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(maxFrameSize).respond();
+            peer.expectBegin().withIncomingWindow(expectedSessionWindow).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(expectedSessionWindow);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(maxFrameSize);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(readBufferSize);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenStreamReceiverWithLinCapabilities() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                               .withSource().withCapabilities("queue")
+                               .withDistributionMode(nullValue())
+                               .and().respond();
+            peer.expectFlow();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("StreamReceiver test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiverOptions receiverOptions = new StreamReceiverOptions();
+            receiverOptions.sourceOptions().capabilities("queue");
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", receiverOptions);
+
+            receiver.openFuture().get();
+
+            assertSame(container, receiver.client());
+            assertSame(connection, receiver.connection());
+
+            receiver.close();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCreateStreamDeliveryWithoutAnyIncomingDeliveryPresent() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            StreamDelivery delivery = receiver.receive(5, TimeUnit.MILLISECONDS);
+
+            assertNull(delivery);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReceiverAwaitTimedCanBePerformedMultipleTimes() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            assertNull(receiver.receive(3, TimeUnit.MILLISECONDS));
+            assertNull(receiver.receive(3, TimeUnit.MILLISECONDS));
+            assertNull(receiver.receive(3, TimeUnit.MILLISECONDS));
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveFailsWhenLinkRemotelyClosed() throws Exception {
+        doTestReceiveFailsWhenLinkRemotelyClose(false);
+    }
+
+    @Test
+    public void testTimedReceiveFailsWhenLinkRemotelyClosed() throws Exception {
+        doTestReceiveFailsWhenLinkRemotelyClose(true);
+    }
+
+    private void doTestReceiveFailsWhenLinkRemotelyClose(boolean timed) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.remoteDetach().later(50);
+
+            if (timed) {
+                assertThrows(ClientLinkRemotelyClosedException.class, () -> receiver.receive(1, TimeUnit.MINUTES));
+            } else {
+                assertThrows(ClientLinkRemotelyClosedException.class, () -> receiver.receive());
+            }
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryUsesUnsettledDeliveryOnOpen() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.remoteDisposition().withRole(Role.SENDER.getValue())
+                                    .withFirst(0)
+                                    .withSettled(true)
+                                    .withState(Accepted.getInstance()).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            final StreamDelivery delivery = receiver.receive();
+
+            Wait.assertTrue("Should eventually be remotely settled", delivery::remoteSettled);
+            Wait.assertTrue(() -> { return delivery.remoteState() == DeliveryState.accepted(); });
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryReceiveWithTransferAlreadyComplete() throws Exception {
+        doTestStreamDeliveryReceiveWithTransferAlreadyComplete(false);
+    }
+
+    @Test
+    public void testStreamDeliveryTryReceiveWithTransferAlreadyComplete() throws Exception {
+        doTestStreamDeliveryReceiveWithTransferAlreadyComplete(true);
+    }
+
+    private void doTestStreamDeliveryReceiveWithTransferAlreadyComplete(boolean tryReceive) throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+
+            // Ensures that stream receiver has the delivery in its queue.
+            connection.openSender("test-sender").openFuture().get();
+
+            final StreamDelivery delivery;
+
+            if (tryReceive) {
+                delivery = receiver.tryReceive();
+            } else {
+                delivery = receiver.receive();
+            }
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryReceivedWhileTransferIsIncomplete() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertFalse(delivery.completed());
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).now();
+
+            Wait.assertTrue("Should eventually be marked as completed", delivery::completed);
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamWithCompleteDeliveryReadByte() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload.length, stream.available());
+            final byte[] deliveryBytes = new byte[payload.length];
+            for (int i = 0; i < payload.length; ++i) {
+                deliveryBytes[i] = (byte) stream.read();
+            }
+
+            assertArrayEquals(payload, deliveryBytes);
+            assertEquals(0, stream.available());
+            assertEquals(-1, stream.read());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamBehaviorAfterStreamClosed() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            stream.close();
+
+            final byte[] scratch = new byte[10];
+
+            assertThrows(IOException.class, () -> stream.available());
+            assertThrows(IOException.class, () -> stream.skip(1));
+            assertThrows(IOException.class, () -> stream.read());
+            assertThrows(IOException.class, () -> stream.read(scratch));
+            assertThrows(IOException.class, () -> stream.read(scratch, 0, scratch.length));
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamWithCompleteDeliveryReadBytes() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload.length, stream.available());
+            final byte[] deliveryBytes = new byte[payload.length];
+            stream.read(deliveryBytes);
+
+            assertArrayEquals(payload, deliveryBytes);
+            assertEquals(0, stream.available());
+            assertEquals(-1, stream.read());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamWithInCompleteDeliveryReadBytes() throws Exception {
+        final byte[] payload1 = createEncodedMessage(new Data(new byte[] { 0, 1, 2, 3, 4, 5 }));
+        final byte[] payload2 = createEncodedMessage(new Data(new byte[] { 6, 7, 8, 9, 0 ,1 }));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertFalse(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload1.length, stream.available());
+            final byte[] deliveryBytes1 = new byte[payload1.length];
+            stream.read(deliveryBytes1);
+
+            assertArrayEquals(payload1, deliveryBytes1);
+            assertEquals(0, stream.available());
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withPayload(payload2).later(50);
+
+            // Should block until more data arrives.
+            final byte[] deliveryBytes2 = new byte[payload2.length];
+            stream.read(deliveryBytes2);
+
+            assertArrayEquals(payload2, deliveryBytes2);
+            assertEquals(0, stream.available());
+
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamReadBytesSignalsEOFOnEmptyCompleteTransfer() throws Exception {
+        final byte[] payload1 = createEncodedMessage(new Data(new byte[] { 0, 1, 2, 3, 4, 5 }));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertFalse(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload1.length, stream.available());
+            final byte[] deliveryBytes1 = new byte[payload1.length];
+            stream.read(deliveryBytes1);
+
+            assertArrayEquals(payload1, deliveryBytes1);
+            assertEquals(0, stream.available());
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .later(50);
+
+            // Should block until more data arrives.
+            final byte[] deliveryBytes2 = new byte[payload1.length];
+            assertEquals(-1, stream.read(deliveryBytes2));
+            assertEquals(0, stream.available());
+
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamWithInCompleteDeliverySkipBytes() throws Exception {
+        final byte[] payload1 = createEncodedMessage(new Data(new byte[] { 0, 1, 2, 3, 4, 5 }));
+        final byte[] payload2 = createEncodedMessage(new Data(new byte[] { 6, 7, 8, 9, 0 ,1 }));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertFalse(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload1.length, stream.available());
+            stream.skip(payload1.length);
+            assertEquals(0, stream.available());
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withPayload(payload2).later(50);
+
+            // Should block until more data arrives.
+            stream.skip(payload2.length);
+            assertEquals(0, stream.available());
+
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamReadOpensSessionWindowForAdditionalInput() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            InputStream rawStream = delivery.rawInputStream();
+            assertNotNull(rawStream);
+
+            // An initial frame has arrived but more than that is requested so the first chuck is pulled
+            // from the incoming delivery and the session window opens which allows the second chunk to
+            // arrive and again the session window will be opened as that chunk is moved to the reader's
+            // buffer for return from the read request.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(9);
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+
+            byte[] combinedPayloads = new byte[payload1.length + payload2.length];
+            rawStream.read(combinedPayloads);
+
+            assertTrue(Arrays.equals(payload1, 0, payload1.length, combinedPayloads, 0, payload1.length));
+            assertTrue(Arrays.equals(payload2, 0, payload2.length, combinedPayloads, payload1.length, payload1.length + payload2.length));
+
+            rawStream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamBlockedReadBytesAborted() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertFalse(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload.length, stream.available());
+            final byte[] deliveryBytes = new byte[payload.length * 2];
+
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withAborted(true)
+                                 .withMessageFormat(0).later(50);
+
+            try {
+                stream.read(deliveryBytes);
+                fail("Delivery should have been aborted while waiting for more data.");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientDeliveryAbortedException);
+            }
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamClosedWithoutReadsConsumesTransfers() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            InputStream rawStream = delivery.rawInputStream();
+            assertNotNull(rawStream);
+
+            // An initial frame has arrived but no reads have been performed and then if closed
+            // the delivery will be consumed to allow the session window to be opened and prevent
+            // a stall due to an un-consumed delivery.  The stream delivery will not auto accept
+            // or auto settle the delivery as the user closed early which should indicate they
+            // are rejecting the message otherwise it is a programming error on their part.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(9);
+
+            rawStream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryRawInputStreamClosedWithoutReadsAllowsUserDisposition() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            InputStream rawStream = delivery.rawInputStream();
+            assertNotNull(rawStream);
+
+            // An initial frame has arrived but no reads have been performed and then if closed
+            // the delivery will be consumed to allow the session window to be opened and prevent
+            // a stall due to an un-consumed delivery.  The stream delivery will not auto accept
+            // or auto settle the delivery as the user closed early which should indicate they
+            // are rejecting the message otherwise it is a programming error on their part.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(9);
+
+            rawStream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("invalid-format", "decode error").withSettled(true);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            delivery.disposition(new ClientDeliveryState.ClientRejected("invalid-format", "decode error"), true);
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryUserAppliedDispositionBeforeStreamRead() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            delivery.disposition(ClientDeliveryState.ClientAccepted.getInstance(), true);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+
+            assertEquals(payload.length, stream.available());
+            final byte[] deliveryBytes = new byte[payload.length];
+            for (int i = 0; i < payload.length; ++i) {
+                deliveryBytes[i] = (byte) stream.read();
+            }
+
+            assertArrayEquals(payload, deliveryBytes);
+            assertEquals(0, stream.available());
+            assertEquals(-1, stream.read());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamSupportsMark() throws Exception {
+        final byte[] payload = createEncodedMessage(new Data(new byte[] { 0, 1, 2, 3, 4, 5 }));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            final InputStream stream = delivery.rawInputStream();
+            assertNotNull(stream);
+            assertTrue(stream.markSupported());
+
+            assertEquals(payload.length, stream.available());
+            stream.mark(payload.length);
+
+            final byte[] deliveryBytes1 = new byte[payload.length];
+            final byte[] deliveryBytes2 = new byte[payload.length];
+            stream.read(deliveryBytes1);
+            stream.reset();
+            stream.read(deliveryBytes2);
+
+            assertNotSame(deliveryBytes1, deliveryBytes2);
+            assertArrayEquals(payload, deliveryBytes1);
+            assertArrayEquals(payload, deliveryBytes2);
+            assertEquals(0, stream.available());
+
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            stream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamMessageWithHeaderOnly() throws Exception {
+        final byte[] payload = createEncodedMessage(new Header().setDurable(true));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+            Header header = message.header();
+            assertNotNull(header);
+
+            assertSame(receiver, message.receiver());
+            assertSame(delivery, message.delivery());
+
+            assertNull(message.properties());
+            assertNull(message.annotations());
+            assertNull(message.applicationProperties());
+            assertNull(message.footer());
+            assertTrue(message.completed());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadHeaderFromStreamMessageWithoutHeaderSection() throws Exception {
+        Map<Symbol, Object> annotationsMap = new HashMap<>();
+        annotationsMap.put(Symbol.valueOf("test-1"), UUID.randomUUID());
+        annotationsMap.put(Symbol.valueOf("test-2"), UUID.randomUUID());
+        annotationsMap.put(Symbol.valueOf("test-3"), UUID.randomUUID());
+
+        final byte[] payload = createEncodedMessage(new MessageAnnotations(annotationsMap));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+            Header header = message.header();
+            assertNull(header);
+            MessageAnnotations annotations = message.annotations();
+            assertNotNull(annotations);
+            assertEquals(annotationsMap, annotations.getValue());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTryReadSectionBeyondWhatIsEncodedIntoMessage() throws Exception {
+        Map<Symbol, Object> annotationsMap = new HashMap<>();
+        annotationsMap.put(Symbol.valueOf("test-1"), UUID.randomUUID());
+        annotationsMap.put(Symbol.valueOf("test-2"), UUID.randomUUID());
+        annotationsMap.put(Symbol.valueOf("test-3"), UUID.randomUUID());
+
+        final byte[] payload = createEncodedMessage(new Header(), new MessageAnnotations(annotationsMap));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            Properties properties = message.properties();
+            assertNull(properties);
+            Header header = message.header();
+            assertNotNull(header);
+            MessageAnnotations annotations = message.annotations();
+            assertNotNull(annotations);
+            assertEquals(annotationsMap, annotations.getValue());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadBytesFromBodyInputStreamUsingReadByteAPI() throws Exception {
+        final byte[] body = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] payload = createEncodedMessage(new Data(body));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertNull(message.header());
+            assertNull(message.annotations());
+            assertNull(message.properties());
+            assertNull(delivery.annotations());
+
+            final byte[] receivedBody = new byte[body.length];
+            for (int i = 0; i < body.length; ++i) {
+                receivedBody[i] = (byte) bodyStream.read();
+            }
+            assertArrayEquals(body, receivedBody);
+            assertEquals(-1, bodyStream.read());
+            assertNull(message.footer());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadBytesFromInputStreamUsingReadByteWithSingleByteSplitTransfers() throws Exception {
+        testReadBytesFromBodyInputStreamWithSplitSingleByteTransfers(1);
+    }
+
+    @Test
+    public void testReadBytesFromInputStreamUsingSingleReadBytesWithSingleByteSplitTransfers() throws Exception {
+        testReadBytesFromBodyInputStreamWithSplitSingleByteTransfers(2);
+    }
+
+    @Test
+    public void testSkipBytesFromInputStreamWithSingleByteSplitTransfers() throws Exception {
+        testReadBytesFromBodyInputStreamWithSplitSingleByteTransfers(3);
+    }
+
+    private void testReadBytesFromBodyInputStreamWithSplitSingleByteTransfers(int option) throws Exception {
+        final byte[] body = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] payload = createEncodedMessage(new Data(body));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            for (int i = 0; i < payload.length; ++i) {
+                peer.remoteTransfer().withHandle(0)
+                                     .withDeliveryId(0)
+                                     .withDeliveryTag(new byte[] { 1 })
+                                     .withMore(true)
+                                     .withMessageFormat(0)
+                                     .withPayload(new byte[] { payload[i] }).afterDelay(3).queue();
+            }
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0).afterDelay(5).queue();
+            peer.expectDisposition().withFirst(0).withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+            final InputStream bodyStream = message.body();
+
+            final byte[] receivedBody = new byte[body.length];
+
+            if (option == 1) {
+                for (int i = 0; i < body.length; ++i) {
+                    receivedBody[i] = (byte) bodyStream.read();
+                }
+                assertArrayEquals(body, receivedBody);
+            } else if (option == 2) {
+                assertEquals(body.length, bodyStream.read(receivedBody));
+                assertArrayEquals(body, receivedBody);
+            } else if (option == 3) {
+                assertEquals(body.length, bodyStream.skip(body.length));
+            } else {
+                fail("Unknown test option");
+            }
+
+            bodyStream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReceiverSessionCannotCreateNewResources() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openReceiver("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openReceiver("test", new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openDurableReceiver("test", "test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openDurableReceiver("test", "test", new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openDynamicReceiver());
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openDynamicReceiver(new HashMap<>()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openDynamicReceiver(new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openDynamicReceiver(new HashMap<>(), new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openSender("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openSender("test", new SenderOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openAnonymousSender());
+            assertThrows(ClientUnsupportedOperationException.class, () -> receiver.session().openAnonymousSender(new SenderOptions()));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadByteArrayPayloadInChunksFromSingleTransferMessage() throws Exception {
+        testReadPayloadInChunksFromLargerMessage(false);
+    }
+
+    @Test
+    public void testReadBytesWithArgsPayloadInChunksFromSingleTransferMessage() throws Exception {
+        testReadPayloadInChunksFromLargerMessage(true);
+    }
+
+    private void testReadPayloadInChunksFromLargerMessage(boolean readWithArgs) throws Exception {
+        final byte[] body = new byte[100];
+        final Random random = new Random();
+        random.setSeed(System.currentTimeMillis());
+        random.nextBytes(body);
+        final byte[] payload = createEncodedMessage(new Data(body));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+            assertEquals(0, delivery.messageFormat());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.messageFormat(1));
+            assertNull(message.header());
+            assertNull(message.annotations());
+            assertNull(message.properties());
+            assertNull(delivery.annotations());
+
+            final byte[] aggregateBody = new byte[body.length];
+            final byte[] receivedBody = new byte[10];
+
+            for (int i = 0; i < body.length; i += 10) {
+                if (readWithArgs) {
+                    bodyStream.read(receivedBody, 0, receivedBody.length);
+                } else {
+                    bodyStream.read(receivedBody);
+                }
+
+                System.arraycopy(receivedBody, 0, aggregateBody, i, receivedBody.length);
+            }
+
+            assertArrayEquals(body, aggregateBody);
+            assertEquals(-1, bodyStream.read(receivedBody, 0, receivedBody.length));
+            assertEquals(-1, bodyStream.read(receivedBody));
+            assertEquals(-1, bodyStream.read());
+            assertNull(message.footer());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReceiverMessageThrowsOnAnyMessageModificationAPI() throws Exception {
+        final byte[] body = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] payload = createEncodedMessage(new Data(body));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.header(new Header()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.properties(new Properties()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.applicationProperties(new ApplicationProperties(null)));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.annotations(new MessageAnnotations(null)));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.footer(new Footer(null)));
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.messageFormat(1));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.durable(true));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.priority((byte) 4));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.timeToLive(128));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.firstAcquirer(false));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.deliveryCount(10));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.messageId(10));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.correlationId(10));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.userId(new byte[] {1}));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.to("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.subject("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.replyTo("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.contentType("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.contentEncoding("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.absoluteExpiryTime(10));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.creationTime(10));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.groupId("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.groupSequence(10));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.replyToGroupId("test"));
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.annotation("test", 1));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.removeAnnotation("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.property("test", 1));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.removeProperty("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.footer("test", 1));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.removeFooter("test"));
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.body(InputStream.nullInputStream()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.addBodySection(new AmqpValue<>("test")));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.bodySections(Collections.emptyList()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.bodySections());
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.clearBodySections());
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.forEachBodySection((section) -> {}));
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.encode(Collections.emptyMap()));
+
+            InputStream bodyStream = message.body();
+
+            assertNotNull(bodyStream.readAllBytes());
+            bodyStream.close();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSkipPayloadInChunksFromSingleTransferMessage() throws Exception {
+        final byte[] body = new byte[100];
+        final Random random = new Random();
+        random.setSeed(System.currentTimeMillis());
+        random.nextBytes(body);
+        final byte[] payload = createEncodedMessage(new Data(body));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertNull(message.header());
+            assertNull(message.annotations());
+            assertNull(message.properties());
+            assertNull(delivery.annotations());
+
+            final int skipSize = 10;
+
+            for (int i = 0; i < body.length; i += skipSize) {
+                bodyStream.skip(10);
+            }
+
+            final byte[] scratchBuffer = new byte[10];
+
+            assertEquals(-1, bodyStream.read(scratchBuffer, 0, scratchBuffer.length));
+            assertEquals(-1, bodyStream.read(scratchBuffer));
+            assertEquals(-1, bodyStream.read());
+            assertNull(message.footer());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadByteArrayPayloadInChunksFromMultipleTransfersMessage() throws Exception {
+        testReadPayloadInChunksFromLargerMultiTransferMessage(false);
+    }
+
+    @Test
+    public void testReadBytesWithArgsPayloadInChunksFromMultipleTransferMessage() throws Exception {
+        testReadPayloadInChunksFromLargerMultiTransferMessage(true);
+    }
+
+    private void testReadPayloadInChunksFromLargerMultiTransferMessage(boolean readWithArgs) throws Exception {
+        final Random random = new Random();
+        final long seed = System.currentTimeMillis();
+        final int numChunks = 4;
+        final int chunkSize = 30;
+
+        random.setSeed(seed);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            for (int i = 0; i < numChunks; ++i) {
+                final byte[] chunk = new byte[chunkSize];
+                random.nextBytes(chunk);
+                peer.remoteTransfer().withHandle(0)
+                                     .withDeliveryId(0)
+                                     .withDeliveryTag(new byte[] { 1 })
+                                     .withMore(true)
+                                     .withMessageFormat(0)
+                                     .withPayload(createEncodedMessage(new Data(chunk))).queue();
+            }
+            peer.remoteTransfer().withHandle(0).withMore(false).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertNull(message.header());
+            assertNull(message.annotations());
+            assertNull(message.properties());
+            assertNull(delivery.annotations());
+
+            final byte[] readChunk = new byte[chunkSize];
+            final byte[] receivedBody = new byte[3];
+
+            random.setSeed(seed);
+
+            int totalBytesRead = 0;
+
+            for (int i = 0; i < numChunks; ++i) {
+                for (int j = 0; j < readChunk.length; j += receivedBody.length) {
+                    int bytesRead = 0;
+                    if (readWithArgs) {
+                        bytesRead = bodyStream.read(receivedBody, 0, receivedBody.length);
+                    } else {
+                        bytesRead = bodyStream.read(receivedBody);
+                    }
+
+                    totalBytesRead += bytesRead;
+
+                    System.arraycopy(receivedBody, 0, readChunk, j, bytesRead);
+                }
+
+                final byte[] chunk = new byte[chunkSize];
+                random.nextBytes(chunk);
+                assertArrayEquals(chunk, readChunk);
+            }
+
+            assertEquals(chunkSize * numChunks, totalBytesRead);
+            assertEquals(-1, bodyStream.read(receivedBody, 0, receivedBody.length));
+            assertEquals(-1, bodyStream.read(receivedBody));
+            assertEquals(-1, bodyStream.read());
+            assertNull(message.footer());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadPayloadFromSplitFrameTransferWithBufferLargerThanTotalPayload() throws Exception {
+        final Random random = new Random();
+        final long seed = System.currentTimeMillis();
+        final int numChunks = 4;
+        final int chunkSize = 30;
+
+        random.setSeed(seed);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            for (int i = 0; i < numChunks; ++i) {
+                final byte[] chunk = new byte[chunkSize];
+                random.nextBytes(chunk);
+                peer.remoteTransfer().withHandle(0)
+                                     .withDeliveryId(0)
+                                     .withDeliveryTag(new byte[] { 1 })
+                                     .withMore(true)
+                                     .withMessageFormat(0)
+                                     .withPayload(createEncodedMessage(new Data(chunk))).queue();
+            }
+            peer.remoteTransfer().withHandle(0).withMore(false).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertNull(message.header());
+            assertNull(message.annotations());
+            assertNull(message.properties());
+            assertNull(delivery.annotations());
+
+            final byte[] receivedBody = new byte[(chunkSize * numChunks) + 100];
+            Arrays.fill(receivedBody, (byte) 0);
+            final int totalBytesRead = bodyStream.read(receivedBody);
+
+            assertEquals(chunkSize * numChunks, totalBytesRead);
+            assertEquals(-1, bodyStream.read(receivedBody, 0, receivedBody.length));
+            assertEquals(-1, bodyStream.read(receivedBody));
+            assertEquals(-1, bodyStream.read());
+            assertNull(message.footer());
+
+            // Regenerate what should have been sent plus empty trailing section to
+            // check that the read doesn't write anything into the area we gave beyond
+            // what was expected payload size.
+            random.setSeed(seed);
+            final byte[] regeneratedPayload = new byte[numChunks * chunkSize + 100];
+            Arrays.fill(regeneratedPayload, (byte) 0);
+            for (int i = 0; i < numChunks; ++i) {
+                final byte[] chunk = new byte[chunkSize];
+                random.nextBytes(chunk);
+                System.arraycopy(chunk, 0, regeneratedPayload, chunkSize * i, chunkSize);
+            }
+
+            assertArrayEquals(regeneratedPayload, receivedBody);
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReadOpensSessionWindowForAdditionalInput() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            // Creating the input stream instance should read the first chunk of data from the incoming
+            // delivery which should result in a new credit being available to expand the session window.
+            // An additional transfer should be placed into the delivery buffer but not yet read since
+            // the user hasn't read anything.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            // Once the read of all data completes the session window should be opened and the
+            // stream should mark the delivery as accepted and settled since we are in auto settle
+            // mode and there is nothing more to read.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(9);
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+
+            byte[] combinedPayloads = new byte[body1.length + body2.length];
+            bodyStream.read(combinedPayloads);
+
+            assertTrue(Arrays.equals(body1, 0, body1.length, combinedPayloads, 0, body1.length));
+            assertTrue(Arrays.equals(body2, 0, body2.length, combinedPayloads, body1.length, body1.length + body2.length));
+
+            bodyStream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReadOpensSessionWindowForAdditionalInputAndGrantsCreditOnClose() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(1);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000).creditWindow(1);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            // Creating the input stream instance should read the first chunk of data from the incoming
+            // delivery which should result in a new credit being available to expand the session window.
+            // An additional transfer should be placed into the delivery buffer but not yet read since
+            // the user hasn't read anything.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(1);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            // Once the read of all data completes the session window should be opened and the
+            // stream should mark the delivery as accepted and settled since we are in auto settle
+            // mode and there is nothing more to read.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(0);
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(1);
+
+            byte[] combinedPayloads = new byte[body1.length + body2.length];
+            bodyStream.read(combinedPayloads);
+
+            assertTrue(Arrays.equals(body1, 0, body1.length, combinedPayloads, 0, body1.length));
+            assertTrue(Arrays.equals(body2, 0, body2.length, combinedPayloads, body1.length, body1.length + body2.length));
+
+            bodyStream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReadOfAllPayloadConsumesTrailingFooterOnClose() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+        final Footer footers = new Footer(new HashMap<>());
+        footers.getValue().put(Symbol.valueOf("footer-key"), "test");
+        final byte[] payload3 = createEncodedMessage(footers);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            assertNotNull(delivery);
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            // Creating the input stream instance should read the first chunk of data from the incoming
+            // delivery which should result in a new credit being available to expand the session window.
+            // An additional transfer should be placed into the delivery buffer but not yet read since
+            // the user hasn't read anything.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            // Once the read of all data completes the session window should be opened and the
+            // stream should mark the delivery as accepted and settled since we are in auto settle
+            // mode and there is nothing more to read.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(10);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload3).queue();
+            peer.expectFlow().withDeliveryCount(1).withIncomingWindow(1).withLinkCredit(9);
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+
+            byte[] combinedPayloads = new byte[body1.length + body2.length];
+            bodyStream.read(combinedPayloads);
+
+            assertTrue(Arrays.equals(body1, 0, body1.length, combinedPayloads, 0, body1.length));
+            assertTrue(Arrays.equals(body2, 0, body2.length, combinedPayloads, body1.length, body1.length + body2.length));
+
+            bodyStream.close();
+
+            Footer footer = message.footer();
+            assertNotNull(footer);
+            assertFalse(footer.getValue().isEmpty());
+            assertTrue(footer.getValue().containsKey(Symbol.valueOf("footer-key")));
+
+            assertTrue(message.hasFooters());
+            assertTrue(message.hasFooter("footer-key"));
+            message.forEachFooter((key, value) -> {
+                assertEquals(key, "footer-key");
+                assertEquals(value, "test");
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.openFuture().get();
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadBytesFromBodyInputStreamWithinTransactedSession() throws Exception {
+        final byte[] body = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] payload = createEncodedMessage(new Data(body));
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectDisposition().withSettled(true).withState().transactional().withTxnId(txnId).withAccepted();
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            receiver.session().beginTransaction();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertNull(message.header());
+            assertNull(message.annotations());
+            assertNull(message.properties());
+            assertNull(delivery.annotations());
+
+            final byte[] receivedBody = new byte[body.length];
+            for (int i = 0; i < body.length; ++i) {
+                receivedBody[i] = (byte) bodyStream.read();
+            }
+            assertArrayEquals(body, receivedBody);
+            assertEquals(-1, bodyStream.read());
+
+            receiver.session().commitTransaction();
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryHandlesInvalidHeaderEncoding() throws Exception {
+        final byte[] payload = createInvalidHeaderEncoding();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("decode-error", "failed reading message header");
+
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientException.class, () -> message.header());
+            assertThrows(ClientException.class, () -> message.body());
+
+            delivery.reject("decode-error", "failed reading message header");
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryHandlesInvalidDeliveryAnnotationsEncoding() throws Exception {
+        final byte[] payload = createInvalidDeliveryAnnotationsEncoding();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("decode-error", "failed reading message header");
+
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientException.class, () -> delivery.annotations());
+            assertThrows(ClientException.class, () -> message.body());
+
+            delivery.reject("decode-error", "failed reading message header");
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryHandlesInvalidMessageAnnotationsEncoding() throws Exception {
+        final byte[] payload = createInvalidMessageAnnotationsEncoding();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("decode-error", "failed reading message header");
+
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientException.class, () -> message.annotations());
+            assertThrows(ClientException.class, () -> message.body());
+
+            delivery.reject("decode-error", "failed reading message header");
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryHandlesInvalidPropertiesEncoding() throws Exception {
+        final byte[] payload = createInvalidPropertiesEncoding();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("decode-error", "failed reading message header");
+
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientException.class, () -> message.properties());
+            assertThrows(ClientException.class, () -> message.body());
+
+            delivery.reject("decode-error", "failed reading message header");
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryHandlesInvalidApplicationPropertiesEncoding() throws Exception {
+        final byte[] payload = createInvalidApplicationPropertiesEncoding();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("decode-error", "failed reading message header");
+
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientException.class, () -> message.applicationProperties());
+            assertThrows(ClientException.class, () -> message.body());
+
+            delivery.reject("decode-error", "failed reading message header");
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamDeliveryHandlesInvalidHeaderEncodingDuringBodyStreamOpen() throws Exception {
+        final byte[] payload = createInvalidHeaderEncoding();
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDisposition().withState().rejected("decode-error", "failed reading message header");
+
+            final StreamDelivery delivery = receiver.receive();
+
+            StreamReceiverMessage message = delivery.message();
+
+            assertThrows(ClientException.class, () -> message.body());
+
+            delivery.reject("decode-error", "failed reading message header");
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testConnectionDropsDuringStreamedBodyRead() throws Exception {
+        final byte[] body1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] body2 = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+        final byte[] payload1 = createEncodedMessage(new Data(body1));
+        final byte[] payload2 = createEncodedMessage(new Data(body2));
+
+        final CountDownLatch disconnected = new CountDownLatch(1);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(1000).respond();
+            peer.expectBegin().withIncomingWindow(1).respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withIncomingWindow(1).withLinkCredit(1);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(1000);
+            connectionOptions.disconnectedHandler((conn, event) -> disconnected.countDown());
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().readBufferSize(2000).creditWindow(1);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            StreamReceiverMessage message = delivery.message();
+
+            // Creating the input stream instance should read the first chunk of data from the incoming
+            // delivery which should result in a new credit being available to expand the session window.
+            // An additional transfer should be placed into the delivery buffer but not yet read since
+            // the user hasn't read anything.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectFlow().withDeliveryCount(0).withIncomingWindow(1).withLinkCredit(1);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).queue();
+            peer.dropAfterLastHandler();
+
+            InputStream bodyStream = message.body();
+            assertNotNull(bodyStream);
+
+            assertTrue(disconnected.await(5, TimeUnit.SECONDS));
+
+            byte[] readPayload = new byte[body1.length + body2.length];
+
+            try {
+                bodyStream.read(readPayload);
+                fail("Should not be able to read from closed connection stream");
+            } catch (IOException ioe) {
+                // Connection should be down now.
+            }
+
+            bodyStream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testFrameSizeViolationWhileWaitingForIncomingStreamReceiverContent() throws Exception {
+        byte[] overFrameSizeLimitFrameHeader = new byte[] { 0x00, (byte) 0xA0, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 };
+
+        final byte[] body = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] payload = createEncodedMessage(new Data(body));
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().withMaxFrameSize(65535).respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow().withLinkCredit(1);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(true)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().maxFrameSize(65535);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            StreamReceiverOptions streamOptions = new StreamReceiverOptions().creditWindow(1);
+            StreamReceiver receiver = connection.openStreamReceiver("test-queue", streamOptions);
+            StreamDelivery delivery = receiver.receive();
+            StreamReceiverMessage message = delivery.message();
+            InputStream stream = message.body();
+
+            peer.waitForScriptToComplete();
+            peer.expectClose().respond();
+            peer.remoteBytes().withBytes(overFrameSizeLimitFrameHeader).later(10);
+
+            byte[] bytesToRead = new byte[body.length * 2];
+
+            try {
+                stream.read(bytesToRead);
+                fail("Should throw an error indicating issue with read of payload");
+            } catch (IOException ioe) {
+                // Expected
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamReceiverTryReadAmqpSequenceBytes() throws Exception {
+        final List<String> stringList = new ArrayList<>();
+        stringList.add("Hello World");
+        final byte[] payload = createEncodedMessage(new AmqpSequence<>(stringList));
+
+        doTestStreamReceiverReadsNonDataSectionBody(payload);
+    }
+
+    @Test
+    public void testStreamReceiverTryReadAmqpValueBytes() throws Exception {
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+        doTestStreamReceiverReadsNonDataSectionBody(payload);
+    }
+
+    private void doTestStreamReceiverReadsNonDataSectionBody(byte[] payload) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withSettled(true)
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            final StreamDelivery delivery = receiver.receive();
+            final StreamReceiverMessage message = delivery.message();
+            try {
+                message.body();
+                fail("Should not return a stream since we cannot readl this type");
+            } catch (ClientException cliEx) {
+                // Expected
+            }
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadMessageHeaderFromStreamReceiverMessage() throws Exception {
+        final Header header = new Header();
+
+        header.setDeliveryCount(UnsignedInteger.MAX_VALUE.longValue());
+        header.setDurable(true);
+        header.setFirstAcquirer(false);
+        header.setPriority((byte) 255);
+        header.setTimeToLive(Integer.MAX_VALUE);
+
+        final byte[] payload = createEncodedMessage(header);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            Header readHeader = message.header();
+            assertNotNull(readHeader);
+            assertNull(message.body());
+
+            assertEquals(Integer.toUnsignedLong(Integer.MAX_VALUE), message.timeToLive());
+            assertEquals(true, message.durable());
+            assertEquals(false, message.firstAcquirer());
+            assertEquals((byte) 255, message.priority());
+            assertEquals(Integer.toUnsignedLong(Integer.MAX_VALUE), message.timeToLive());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadMessagePropertiesFromStreamReceiverMessage() throws Exception {
+        final Properties properties = new Properties();
+
+        properties.setAbsoluteExpiryTime(Integer.MAX_VALUE);
+        properties.setContentEncoding("utf8");
+        properties.setContentType("text/plain");
+        properties.setCorrelationId(new byte[] { 1, 2, 3 });
+        properties.setCreationTime(Short.MAX_VALUE);
+        properties.setGroupId("Group");
+        properties.setGroupSequence(UnsignedInteger.MAX_VALUE.longValue());
+        properties.setMessageId(UUID.randomUUID());
+        properties.setReplyTo("replyTo");
+        properties.setReplyToGroupId("group-1");
+        properties.setSubject("test");
+        properties.setTo("queue");
+        properties.setUserId(new byte[] { 0, 1, 5, 6, 9 });
+
+        final byte[] payload = createEncodedMessage(new Header(), properties);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            assertFalse(message.hasProperties());
+            assertFalse(message.hasFooters());
+            assertFalse(message.hasAnnotations());
+
+            Properties readProperties = message.properties();
+            assertNotNull(readProperties);
+            Header header = message.header();
+            assertNotNull(header);
+            assertNull(message.body());
+
+            assertEquals(Integer.MAX_VALUE, message.absoluteExpiryTime());
+            assertEquals("utf8", message.contentEncoding());
+            assertEquals("text/plain", message.contentType());
+            assertArrayEquals(new byte[] { 1, 2, 3 }, (byte[]) message.correlationId());
+            assertEquals("utf8", message.contentEncoding());
+            assertEquals("utf8", message.contentEncoding());
+            assertEquals("utf8", message.contentEncoding());
+            assertEquals(Short.MAX_VALUE, message.creationTime());
+            assertEquals(UnsignedInteger.MAX_VALUE.intValue(), message.groupSequence());
+            assertEquals(properties.getMessageId(), message.messageId());
+            assertEquals("replyTo", message.replyTo());
+            assertEquals("group-1", message.replyToGroupId());
+            assertEquals("test", message.subject());
+            assertEquals("queue", message.to());
+            assertArrayEquals(new byte[] { 0, 1, 5, 6, 9 }, message.userId());
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReadApplicationPropertiesStreamReceiverMessage() throws Exception {
+        final Map<String, Object> propertiesMap = new HashMap<>();
+        final ApplicationProperties appProperties = new ApplicationProperties(propertiesMap);
+
+        propertiesMap.put("property1", UnsignedInteger.MAX_VALUE);
+        propertiesMap.put("property2", UnsignedInteger.ONE);
+        propertiesMap.put("property3", UnsignedInteger.ZERO);
+
+        final byte[] payload = createEncodedMessage(appProperties);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.expectFlow();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withFirst(0).withState().accepted().withSettled(true);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            final Client container = Client.create();
+            final Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            final StreamReceiver receiver = connection.openStreamReceiver("test-queue");
+            final StreamDelivery delivery = receiver.receive();
+
+            assertNotNull(delivery);
+            assertTrue(delivery.completed());
+            assertFalse(delivery.aborted());
+
+            StreamReceiverMessage message = delivery.message();
+            assertNotNull(message);
+
+            assertTrue(message.hasProperties());
+            assertFalse(message.hasFooters());
+            assertFalse(message.hasAnnotations());
+
+            assertFalse(message.hasProperty("property"));
+            assertEquals(UnsignedInteger.MAX_VALUE, message.property("property1"));
+            assertEquals(UnsignedInteger.ONE, message.property("property2"));
+            assertEquals(UnsignedInteger.ZERO, message.property("property3"));
+
+            message.forEachProperty((key, value) -> {
+                assertTrue(propertiesMap.containsKey(key));
+                assertEquals(value, propertiesMap.get(key));
+            });
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenDrainTimeoutExceeded() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiverOptions receiverOptions = new StreamReceiverOptions().drainTimeout(15);
+            Receiver receiver = connection.openStreamReceiver("test-queue", receiverOptions).openFuture().get();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainFutureSignalsFailureWhenConnectionDrainTimeoutExceeded() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions connectionOptions = new ConnectionOptions().drainTimeout(20);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), connectionOptions);
+            Receiver receiver = connection.openStreamReceiver("test-queue").openFuture().get();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientOperationTimedOutException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainCompletesWhenReceiverHasNoCredit() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Receiver receiver = connection.openStreamReceiver("test-queue", new StreamReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(5, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Future<? extends Receiver> draining = receiver.drain();
+            draining.get(5, TimeUnit.SECONDS);
+
+            // Close things down
+            peer.expectClose().respond();
+            connection.closeAsync().get(5, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(1, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDrainAdditionalDrainCallThrowsWhenReceiverStillDraining() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectFlow().withDrain(true);
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiverOptions receiverOptions = new StreamReceiverOptions();
+            Receiver receiver = connection.openStreamReceiver("test-queue", receiverOptions).openFuture().get();
+
+            receiver.drain();
+
+            try {
+                receiver.drain().get();
+                fail("Drain call should fail timeout exceeded.");
+            } catch (ExecutionException cliEx) {
+                LOG.debug("Receiver threw error on drain call", cliEx);
+                assertTrue(cliEx.getCause() instanceof ClientIllegalStateException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetRemotePropertiesWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverRemoteProperties(true);
+    }
+
+    @Test
+    public void testReceiverGetRemotePropertiesFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverRemoteProperties(false);
+    }
+
+    private void tryReadReceiverRemoteProperties(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamReceiverOptions options = new StreamReceiverOptions().openTimeout(100);
+            Receiver receiver = connection.openStreamReceiver("test-receiver", options);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            Map<String, Object> expectedProperties = new HashMap<>();
+            expectedProperties.put("TEST", "test-property");
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.expectEnd().respond();
+                peer.respondToLastAttach().withPropertiesMap(expectedProperties).later(10);
+            } else {
+                peer.expectDetach();
+                peer.expectEnd();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.properties(), "Remote should have responded with a remote properties value");
+                assertEquals(expectedProperties, receiver.properties());
+            } else {
+                try {
+                    receiver.properties();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetRemoteOfferedCapabilitiesWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverRemoteOfferedCapabilities(true);
+    }
+
+    @Test
+    public void testReceiverGetRemoteOfferedCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverRemoteOfferedCapabilities(false);
+    }
+
+    private void tryReadReceiverRemoteOfferedCapabilities(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            StreamReceiver receiver = connection.openStreamReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.expectEnd().respond();
+                peer.respondToLastAttach().withOfferedCapabilities("QUEUE").later(10);
+            } else {
+                peer.expectDetach();
+                peer.expectEnd();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.offeredCapabilities(), "Remote should have responded with a remote offered Capabilities value");
+                assertEquals(1, receiver.offeredCapabilities().length);
+                assertEquals("QUEUE", receiver.offeredCapabilities()[0]);
+            } else {
+                try {
+                    receiver.offeredCapabilities();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetRemoteDesiredCapabilitiesWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverRemoteDesiredCapabilities(true);
+    }
+
+    @Test
+    public void testReceiverGetRemoteDesiredCapabilitiesFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverRemoteDesiredCapabilities(false);
+    }
+
+    private void tryReadReceiverRemoteDesiredCapabilities(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Receiver receiver = connection.openStreamReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.expectEnd().respond();
+                peer.respondToLastAttach().withDesiredCapabilities("Error-Free").later(10);
+            } else {
+                peer.expectDetach();
+                peer.expectEnd();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.desiredCapabilities(), "Remote should have responded with a remote desired Capabilities value");
+                assertEquals(1, receiver.desiredCapabilities().length);
+                assertEquals("Error-Free", receiver.desiredCapabilities()[0]);
+            } else {
+                try {
+                    receiver.desiredCapabilities();
+                    fail("Should failed to get remote state due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetTargetWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverTarget(true);
+    }
+
+    @Test
+    public void testReceiverGetTargetFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverTarget(false);
+    }
+
+    private void tryReadReceiverTarget(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Receiver receiver = connection.openStreamReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.expectEnd().respond();
+                peer.respondToLastAttach().later(10);
+            } else {
+                peer.expectDetach();
+                peer.expectEnd();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.target(), "Remote should have responded with a Target value");
+            } else {
+                try {
+                    receiver.target();
+                    fail("Should failed to get remote source due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiverGetSourceWaitsForRemoteAttach() throws Exception {
+        tryReadReceiverSource(true);
+    }
+
+    @Test
+    public void testReceiverGetSourceFailsAfterOpenTimeout() throws Exception {
+        tryReadReceiverSource(false);
+    }
+
+    private void tryReadReceiverSource(boolean attachResponse) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.RECEIVER.getValue());
+            peer.expectFlow();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().openTimeout(100);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Receiver receiver = connection.openStreamReceiver("test-receiver");
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            if (attachResponse) {
+                peer.expectDetach().respond();
+                peer.expectEnd().respond();
+                peer.respondToLastAttach().later(10);
+            } else {
+                peer.expectDetach();
+                peer.expectEnd();
+            }
+
+            if (attachResponse) {
+                assertNotNull(receiver.source(), "Remote should have responded with a Source value");
+                assertEquals("test-receiver", receiver.source().address());
+            } else {
+                try {
+                    receiver.source();
+                    fail("Should failed to get remote source due to no attach response");
+                } catch (ClientException ex) {
+                    LOG.debug("Caught expected exception from blocking call", ex);
+                }
+            }
+
+            try {
+                receiver.closeAsync().get();
+            } catch (ExecutionException ex) {
+                LOG.debug("Caught unexpected exception from close call", ex);
+                fail("Should not fail close whenn connection not closed and detach sent");
+            }
+
+            peer.expectClose().respond();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    private byte[] createInvalidHeaderEncoding() {
+        final byte[] buffer = new byte[12];
+
+        buffer[0] = 0; // Described Type Indicator
+        buffer[1] = EncodingCodes.SMALLULONG;
+        buffer[2] = Header.DESCRIPTOR_CODE.byteValue();
+        buffer[3] = EncodingCodes.MAP32; // Should be list based
+
+        return buffer;
+    }
+
+    private byte[] createInvalidDeliveryAnnotationsEncoding() {
+        final byte[] buffer = new byte[12];
+
+        buffer[0] = 0; // Described Type Indicator
+        buffer[1] = EncodingCodes.SMALLULONG;
+        buffer[2] = DeliveryAnnotations.DESCRIPTOR_CODE.byteValue();
+        buffer[3] = EncodingCodes.LIST32; // Should be Map based
+
+        return buffer;
+    }
+
+    private byte[] createInvalidMessageAnnotationsEncoding() {
+        final byte[] buffer = new byte[12];
+
+        buffer[0] = 0; // Described Type Indicator
+        buffer[1] = EncodingCodes.SMALLULONG;
+        buffer[2] = MessageAnnotations.DESCRIPTOR_CODE.byteValue();
+        buffer[3] = EncodingCodes.LIST32; // Should be Map based
+
+        return buffer;
+    }
+
+    private byte[] createInvalidPropertiesEncoding() {
+        final byte[] buffer = new byte[12];
+
+        buffer[0] = 0; // Described Type Indicator
+        buffer[1] = EncodingCodes.SMALLULONG;
+        buffer[2] = Properties.DESCRIPTOR_CODE.byteValue();
+        buffer[3] = EncodingCodes.MAP32; // Should be list based
+
+        return buffer;
+    }
+
+    private byte[] createInvalidApplicationPropertiesEncoding() {
+        final byte[] buffer = new byte[12];
+
+        buffer[0] = 0; // Described Type Indicator
+        buffer[1] = EncodingCodes.SMALLULONG;
+        buffer[2] = ApplicationProperties.DESCRIPTOR_CODE.byteValue();
+        buffer[3] = EncodingCodes.LIST32; // Should be map based
+
+        return buffer;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/StreamSenderTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/StreamSenderTest.java
new file mode 100644
index 0000000..b3738da
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/StreamSenderTest.java
@@ -0,0 +1,2791 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.DeliveryMode;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.OutputStreamOptions;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.SenderOptions;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderMessage;
+import org.apache.qpid.protonj2.client.StreamSenderOptions;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientUnsupportedOperationException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.ApplicationPropertiesMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.DeliveryAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.FooterMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.HeaderMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.MessageAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.PropertiesMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedCompositingDataSectionMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedDataMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedPartialDataSectionMatcher;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests the {@link message} implementation
+ */
+@Timeout(20)
+public class StreamSenderTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(StreamSenderTest.class);
+
+    @Test
+    public void testSendWhenCreditIsAvailable() throws Exception {
+        doTestSendWhenCreditIsAvailable(false, false);
+    }
+
+    @Test
+    public void testTrySendWhenCreditIsAvailable() throws Exception {
+        doTestSendWhenCreditIsAvailable(true, false);
+    }
+
+    @Test
+    public void testSendWhenCreditIsAvailableWithDeliveryAnnotations() throws Exception {
+        doTestSendWhenCreditIsAvailable(false, true);
+    }
+
+    @Test
+    public void testTrySendWhenCreditIsAvailableWithDeliveryAnnotations() throws Exception {
+        doTestSendWhenCreditIsAvailable(true, true);
+    }
+
+    private void doTestSendWhenCreditIsAvailable(boolean trySend, boolean addDeliveryAnnotations) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(10)
+                             .withIncomingWindow(1024)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(1).queue();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            sender.openFuture().get(10, TimeUnit.SECONDS);
+
+            // This ensures that the flow to sender is processed before we try-send
+            Receiver receiver = connection.openReceiver("test-queue", new ReceiverOptions().creditWindow(0));
+            receiver.openFuture().get(10, TimeUnit.SECONDS);
+
+            Map<String, Object> deliveryAnnotations = new HashMap<>();
+            deliveryAnnotations.put("da1", 1);
+            deliveryAnnotations.put("da2", 2);
+            deliveryAnnotations.put("da3", 3);
+            DeliveryAnnotationsMatcher daMatcher = new DeliveryAnnotationsMatcher(true);
+            daMatcher.withEntry("da1", Matchers.equalTo(1));
+            daMatcher.withEntry("da2", Matchers.equalTo(2));
+            daMatcher.withEntry("da3", Matchers.equalTo(3));
+            EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World");
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            if (addDeliveryAnnotations) {
+                payloadMatcher.setDeliveryAnnotationsMatcher(daMatcher);
+            }
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNonNullPayload();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            Message<String> message = Message.create("Hello World");
+
+            final Tracker tracker;
+            if (trySend) {
+                if (addDeliveryAnnotations) {
+                    tracker = sender.trySend(message, deliveryAnnotations);
+                } else {
+                    tracker = sender.trySend(message);
+                }
+            } else {
+                if (addDeliveryAnnotations) {
+                    tracker = sender.send(message, deliveryAnnotations);
+                } else {
+                    tracker = sender.send(message);
+                }
+            }
+
+            assertNotNull(tracker);
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenStreamSenderWithLinCapabilities() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue())
+                               .withTarget().withCapabilities("queue").and()
+                               .respond();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("StreamSender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSenderOptions senderOptions = new StreamSenderOptions();
+            senderOptions.targetOptions().capabilities("queue");
+            StreamSender sender = connection.openStreamSender("test-queue", senderOptions);
+
+            sender.openFuture().get();
+            sender.close();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenStreamSenderAppliesDefaultSessionOutgoingWindow() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue())
+                               .withTarget().withCapabilities("queue").and()
+                               .respond();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("StreamSender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSenderOptions senderOptions = new StreamSenderOptions();
+            senderOptions.targetOptions().capabilities("queue");
+            ClientStreamSender sender = (ClientStreamSender) connection.openStreamSender("test-queue", senderOptions);
+
+            assertEquals(StreamSenderOptions.DEFAULT_PENDING_WRITES_BUFFER_SIZE, sender.getProtonSender().getSession().getOutgoingCapacity());
+
+            sender.openFuture().get();
+            sender.close();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenStreamSenderAppliesConfiguredSessionOutgoingWindow() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().withRole(Role.SENDER.getValue())
+                               .withTarget().withCapabilities("queue").and()
+                               .respond();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            final int PENDING_WRITES_BUFFER_SIZE = StreamSenderOptions.DEFAULT_PENDING_WRITES_BUFFER_SIZE / 2;
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("StreamSender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSenderOptions senderOptions = new StreamSenderOptions().pendingWritesBufferSize(PENDING_WRITES_BUFFER_SIZE);
+            senderOptions.targetOptions().capabilities("queue");
+            ClientStreamSender sender = (ClientStreamSender) connection.openStreamSender("test-queue", senderOptions);
+
+            assertEquals(PENDING_WRITES_BUFFER_SIZE, sender.getProtonSender().getSession().getOutgoingCapacity());
+
+            sender.openFuture().get();
+            sender.close();
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendCustomMessageWithMultipleAmqpValueSections() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+            Session session = connection.openSession().openFuture().get();
+
+            StreamSenderOptions options = new StreamSenderOptions();
+            options.deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            options.writeBufferSize(Integer.MAX_VALUE);
+
+            StreamSender sender = connection.openStreamSender("test-qos", options);
+
+            // Create a custom message format send context and ensure that no early buffer writes take place
+            StreamSenderMessage message = sender.beginMessage();
+
+            assertEquals(sender, message.sender());
+            assertNull(message.tracker());
+
+            assertEquals(Header.DEFAULT_PRIORITY, message.priority());
+            assertEquals(Header.DEFAULT_DELIVERY_COUNT, message.deliveryCount());
+            assertEquals(Header.DEFAULT_FIRST_ACQUIRER, message.firstAcquirer());
+            assertEquals(Header.DEFAULT_TIME_TO_LIVE, message.timeToLive());
+            assertEquals(Header.DEFAULT_DURABILITY, message.durable());
+
+            message.messageFormat(17);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            // Note: This is a specification violation but could be used by other message formats
+            //       and we don't attempt to enforce at the Send Context what users write
+            EncodedAmqpValueMatcher bodyMatcher1 = new EncodedAmqpValueMatcher("one", true);
+            EncodedAmqpValueMatcher bodyMatcher2 = new EncodedAmqpValueMatcher("two", true);
+            EncodedAmqpValueMatcher bodyMatcher3 = new EncodedAmqpValueMatcher("three", false);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher1);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher2);
+            payloadMatcher.addMessageContentMatcher(bodyMatcher3);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withMore(false).withMessageFormat(17).withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            message.header(header);
+            message.addBodySection(new AmqpValue<>("one"));
+            message.addBodySection(new AmqpValue<>("two"));
+            message.addBodySection(new AmqpValue<>("three"));
+
+            message.complete();
+
+            assertNotNull(message.tracker());
+            assertEquals(17, message.messageFormat());
+            assertNotNull(message.tracker().settlementFuture().isDone());
+            assertNotNull(message.tracker().settlementFuture().get().settled());
+            assertThrows(ClientIllegalStateException.class, () -> message.addBodySection(new AmqpValue<>("three")));
+            assertThrows(ClientIllegalStateException.class, () -> message.body());
+            assertThrows(ClientIllegalStateException.class, () -> message.rawOutputStream());
+            assertThrows(ClientIllegalStateException.class, () -> message.abort());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testClearBodySectionsIsNoOpForStreamSenderMessage() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+            Session session = connection.openSession().openFuture().get();
+
+            StreamSenderOptions options = new StreamSenderOptions();
+            options.deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            options.writeBufferSize(Integer.MAX_VALUE);
+
+            StreamSender sender = connection.openStreamSender("test-qos", options);
+
+            // Create a custom message format send context and ensure that no early buffer writes take place
+            StreamSenderMessage message = sender.beginMessage();
+
+            message.messageFormat(17);
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            EncodedAmqpValueMatcher bodyMatcher1 = new EncodedAmqpValueMatcher("one", true);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.addMessageContentMatcher(bodyMatcher1);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withMore(false).withMessageFormat(17).withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            message.addBodySection(new AmqpValue<>("one"));
+            message.clearBodySections();
+            message.forEachBodySection((section) -> {
+                // No sections retained so this should never run.
+                throw new RuntimeException();
+            });
+
+            assertNotNull(message.bodySections());
+            assertTrue(message.bodySections().isEmpty());
+
+            message.complete();
+
+            assertNotNull(message.tracker().settlementFuture().isDone());
+            assertNotNull(message.tracker().settlementFuture().get().settled());
+            assertThrows(ClientIllegalStateException.class, () -> message.body());
+            assertThrows(ClientIllegalStateException.class, () -> message.rawOutputStream());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testMessageFormatCannotBeModifiedAfterBodyWritesStart() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            StreamSender sender = connection.openStreamSender("test-qos");
+            StreamSenderMessage message = sender.beginMessage();
+
+            sender.openFuture().get();
+
+            message.durable(true);
+            message.messageFormat(17);
+            message.body();
+
+            try {
+                message.messageFormat(16);
+                fail("Should not be able to modify message format after body writes started");
+            } catch (ClientIllegalStateException ex) {
+                // Expected
+            } catch (Exception unexpected) {
+                fail("Failed test due to message format set throwing unexpected error: " + unexpected);
+            }
+
+            message.abort();
+
+            assertThrows(ClientIllegalStateException.class, () -> message.complete());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotCreateNewStreamingMessageWhileCurrentInstanceIsIncomplete() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            StreamSender sender = (StreamSender) connection.openStreamSender("test-qos").openFuture().get();
+            StreamSenderMessage message = sender.beginMessage();
+
+            try {
+                sender.beginMessage();
+                fail("Should not be able create a new streaming sender message before last one is compelted.");
+            } catch (ClientIllegalStateException ex) {
+                // Expected
+            }
+
+            message.abort();
+
+            assertThrows(ClientIllegalStateException.class, () -> message.complete());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotAssignAnOutputStreamToTheMessageBody() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            StreamSender sender = (StreamSender) connection.openStreamSender("test-qos").openFuture().get();
+            StreamSenderMessage message = sender.beginMessage();
+
+            try {
+                message.body(new ByteArrayOutputStream());
+                fail("Should not be able assign an output stream to the message body");
+            } catch (ClientUnsupportedOperationException ex) {
+                // Expected
+            }
+
+            message.abort();
+
+            assertThrows(ClientIllegalStateException.class, () -> message.complete());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotModifyMessagePreambleAfterWritesHaveStarted() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            StreamSender sender = (StreamSender) connection.openStreamSender("test-qos").openFuture().get();
+            StreamSenderMessage message = sender.beginMessage();
+
+            message.durable(true);
+            message.messageId("test");
+            message.annotation("key", "value");
+            message.property("key", "value");
+            message.body();
+
+            try {
+                message.durable(false);
+                fail("Should not be able to modify message preamble after body writes started");
+            } catch (ClientIllegalStateException ex) {
+                // Expected
+            }
+
+            try {
+                message.messageId("test1");
+                fail("Should not be able to modify message preamble after body writes started");
+            } catch (ClientIllegalStateException ex) {
+                // Expected
+            }
+
+            try {
+                message.annotation("key1", "value");
+                fail("Should not be able to modify message preamble after body writes started");
+            } catch (ClientIllegalStateException ex) {
+                // Expected
+            }
+
+            try {
+                message.property("key", "value");
+                fail("Should not be able to modify message preamble after body writes started");
+            } catch (ClientIllegalStateException ex) {
+                // Expected
+            }
+
+            message.abort();
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testCreateStream() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectTransfer().withMore(false).withNullPayload();
+            peer.expectDetach().withClosed(true).respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-qos");
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = tracker.body(options);
+
+            assertNotNull(stream);
+
+            sender.openFuture().get();
+
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testOutputStreamOptionsEnforcesValidBodySizeValues() throws Exception {
+        OutputStreamOptions options = new OutputStreamOptions();
+
+        options.bodyLength(1024);
+        options.bodyLength(Integer.MAX_VALUE);
+
+        assertThrows(IllegalArgumentException.class, () -> options.bodyLength(-1));
+    }
+
+    @Test
+    public void testFlushWithSetNonBodySectionsThenClose() throws Exception {
+        doTestNonBodySectionWrittenWhenNoWritesToStream(true);
+    }
+
+    @Test
+    public void testCloseWithSetNonBodySections() throws Exception {
+        doTestNonBodySectionWrittenWhenNoWritesToStream(false);
+    }
+
+    private void doTestNonBodySectionWrittenWhenNoWritesToStream(boolean flushBeforeClose) throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            message.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = message.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            if (flushBeforeClose) {
+                peer.expectTransfer().withMore(true).withPayload(payloadMatcher);
+                peer.expectTransfer().withMore(false).withNullPayload()
+                                     .respond()
+                                     .withSettled(true).withState().accepted();
+            } else {
+                peer.expectTransfer().withMore(false).withPayload(payloadMatcher)
+                                     .respond()
+                                     .withSettled(true).withState().accepted();
+            }
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Once flush is called than anything in the buffer is written regardless of
+            // there being any actual stream writes.  Default close action is to complete
+            // the delivery.
+            if (flushBeforeClose) {
+                stream.flush();
+            }
+            stream.close();
+
+            message.tracker().awaitSettlement(10, TimeUnit.SECONDS);
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testFlushAfterFirstWriteEncodesAMQPHeaderAndMessageBuffer() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            message.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = message.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(dataMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withMore(true).withPayload(payloadMatcher);
+            peer.expectTransfer().withMore(false).withNullPayload();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Stream won't output until some body bytes are written since the buffer was not
+            // filled by the header write.  Then the close will complete the stream message.
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testAutoFlushAfterSingleWriteExceedsConfiguredBufferLimit() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue", new StreamSenderOptions().writeBufferSize(512));
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            final byte[] payload = new byte[512];
+            Arrays.fill(payload, (byte) 16);
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            tracker.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher = new EncodedDataMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(dataMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).withMore(true);
+
+            // Stream won't output until some body bytes are written.
+            stream.write(payload);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testAutoFlushDuringWriteThatExceedConfiguredBufferLimit() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue", new StreamSenderOptions().writeBufferSize(256));
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            final byte[] payload = new byte[1024];
+            Arrays.fill(payload, 0, 256, (byte) 1);
+            Arrays.fill(payload, 256, 512, (byte) 2);
+            Arrays.fill(payload, 512, 768, (byte) 3);
+            Arrays.fill(payload, 768, 1024, (byte) 4);
+
+            final byte[] payload1 = new byte[256];
+            Arrays.fill(payload1, (byte) 1);
+            final byte[] payload2 = new byte[256];
+            Arrays.fill(payload2, (byte) 2);
+            final byte[] payload3 = new byte[256];
+            Arrays.fill(payload3, (byte) 3);
+            final byte[] payload4 = new byte[256];
+            Arrays.fill(payload4, (byte) 4);
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            tracker.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(payload1);
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setHeadersMatcher(headerMatcher);
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(payload2);
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            EncodedDataMatcher dataMatcher3 = new EncodedDataMatcher(payload3);
+            TransferPayloadCompositeMatcher payloadMatcher3 = new TransferPayloadCompositeMatcher();
+            payloadMatcher3.setMessageContentMatcher(dataMatcher3);
+
+            EncodedDataMatcher dataMatcher4 = new EncodedDataMatcher(payload4);
+            TransferPayloadCompositeMatcher payloadMatcher4 = new TransferPayloadCompositeMatcher();
+            payloadMatcher4.setMessageContentMatcher(dataMatcher4);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            peer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            peer.expectTransfer().withPayload(payloadMatcher3).withMore(true);
+            peer.expectTransfer().withPayload(payloadMatcher4).withMore(true);
+
+            // Stream won't output until some body bytes are written.
+            stream.write(payload);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            stream.close();
+
+            sender.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testAutoFlushDuringWriteThatExceedConfiguredBufferLimitSessionCreditLimitOnTransfer() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue", new StreamSenderOptions().writeBufferSize(256));
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            final byte[] payload = new byte[1024];
+            Arrays.fill(payload, 0, 256, (byte) 1);
+            Arrays.fill(payload, 256, 512, (byte) 2);
+            Arrays.fill(payload, 512, 768, (byte) 3);
+            Arrays.fill(payload, 768, 1024, (byte) 4);
+
+            final byte[] payload1 = new byte[256];
+            Arrays.fill(payload1, (byte) 1);
+            final byte[] payload2 = new byte[256];
+            Arrays.fill(payload2, (byte) 2);
+            final byte[] payload3 = new byte[256];
+            Arrays.fill(payload3, (byte) 3);
+            final byte[] payload4 = new byte[256];
+            Arrays.fill(payload4, (byte) 4);
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            tracker.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(payload1);
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setHeadersMatcher(headerMatcher);
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(payload2);
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            EncodedDataMatcher dataMatcher3 = new EncodedDataMatcher(payload3);
+            TransferPayloadCompositeMatcher payloadMatcher3 = new TransferPayloadCompositeMatcher();
+            payloadMatcher3.setMessageContentMatcher(dataMatcher3);
+
+            EncodedDataMatcher dataMatcher4 = new EncodedDataMatcher(payload4);
+            TransferPayloadCompositeMatcher payloadMatcher4 = new TransferPayloadCompositeMatcher();
+            payloadMatcher4.setMessageContentMatcher(dataMatcher4);
+
+            final CountDownLatch sendComplete = new CountDownLatch(1);
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            // Stream won't output until some body bytes are written.
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    stream.write(payload);
+                } catch (IOException e) {
+                    LOG.info("send failed with error: ", e);
+                    sendFailed.set(true);
+                } finally {
+                    sendComplete.countDown();
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).queue();
+            peer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).queue();
+            peer.expectTransfer().withPayload(payloadMatcher3).withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).queue();
+            peer.expectTransfer().withPayload(payloadMatcher4).withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(5).withLinkCredit(10).queue();
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Initiate the above script of transfers and flows
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(1).withLinkCredit(10).now();
+
+            assertTrue(sendComplete.await(10, TimeUnit.SECONDS));
+
+            stream.close();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testCloseAfterSingleWriteEncodesAndCompletesTransferWhenNoStreamSizeConfigured() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            tracker.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(dataMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Stream won't output until some body bytes are written.
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testFlushAfterSecondWriteDoesNotEncodeAMQPHeaderFromConfiguration() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            tracker.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setHeadersMatcher(headerMatcher);
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            // Second flush expectation
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(new byte[] { 4, 5, 6, 7 });
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            peer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Stream won't output until some body bytes are written.
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+
+            // Next write should only be a single Data section
+            stream.write(new byte[] { 4, 5, 6, 7 });
+            stream.flush();
+
+            // Final Transfer that completes the Delivery
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testIncompleteStreamClosureCausesTransferAbort() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            final byte[] payload = new byte[] { 0, 1, 2, 3 };
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setDeliveryCount(1);
+
+            tracker.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions().bodyLength(8192);
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withDeliveryCount(1);
+            EncodedPartialDataSectionMatcher partialDataMatcher = new EncodedPartialDataSectionMatcher(8192, payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(partialDataMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher);
+            peer.expectTransfer().withAborted(true).withNullPayload();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            stream.write(payload);
+            stream.flush();
+
+            // Stream should abort the send now since the configured size wasn't sent.
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testIncompleteStreamClosureWithNoWritesAbortsTransfer() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setDeliveryCount(1);
+
+            message.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions().bodyLength(8192).completeSendOnClose(false);
+            OutputStream stream = message.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withDeliveryCount(1);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // This should abort the transfer as we might have triggered output upon create when the
+            // preamble was written.
+            stream.close();
+
+            assertTrue(message.aborted());
+
+            // Should have no affect.
+            message.abort();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testCompleteStreamClosureCausesTransferCompleted() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(3).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            final byte[] payload1 = new byte[] { 0, 1, 2, 3, 4, 5 };
+            final byte[] payload2 = new byte[] { 6, 7, 8, 9, 10, 11, 12, 13, 14 };
+            final byte[] payload3 = new byte[] { 15 };
+
+            final int payloadSize = payload1.length + payload2.length + payload3.length;
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setDeliveryCount(1);
+
+            tracker.header(header);
+
+            // Populate message application properties
+            tracker.property("ap1", 1);
+            tracker.property("ap2", 2);
+            tracker.property("ap3", 3);
+
+            OutputStreamOptions options = new OutputStreamOptions().bodyLength(payloadSize);
+            OutputStream stream = tracker.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withDeliveryCount(1);
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("ap1", Matchers.equalTo(1));
+            apMatcher.withEntry("ap2", Matchers.equalTo(2));
+            apMatcher.withEntry("ap3", Matchers.equalTo(3));
+            EncodedPartialDataSectionMatcher partialDataMatcher = new EncodedPartialDataSectionMatcher(payloadSize, payload1);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(partialDataMatcher);
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher);
+
+            stream.write(payload1);
+            stream.flush();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            partialDataMatcher = new EncodedPartialDataSectionMatcher(payload2);
+            payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(partialDataMatcher);
+            peer.expectTransfer().withMore(true).withPayload(partialDataMatcher);
+
+            stream.write(payload2);
+            stream.flush();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            partialDataMatcher = new EncodedPartialDataSectionMatcher(payload3);
+            payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(partialDataMatcher);
+            peer.expectTransfer().withMore(false).withPayload(partialDataMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            stream.write(payload3);
+            stream.flush();
+
+            // Stream should already be completed so no additional frames should be written.
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testRawOutputStreamFromMessageWritesUnmodifiedBytes() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            OutputStream stream = message.rawOutputStream();
+
+            // Only one writer at a time can exist
+            assertThrows(ClientIllegalStateException.class, () -> message.rawOutputStream());
+            assertThrows(ClientIllegalStateException.class, () -> message.body());
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withMore(true).withPayload(new byte[] { 0, 1, 2, 3 });
+            peer.expectTransfer().withMore(false).withNullPayload();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamSenderMessageWithDeliveryAnnotations() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+
+            // Populate delivery annotations
+            final Map<String, Object> deliveryAnnotations = new HashMap<>();
+            deliveryAnnotations.put("da1", 1);
+            deliveryAnnotations.put("da2", 2);
+            deliveryAnnotations.put("da3", 3);
+
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage(deliveryAnnotations);
+
+            final byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            PropertiesMatcher propertiesMatcher = new PropertiesMatcher(true);
+            propertiesMatcher.withMessageId("ID:12345");
+            propertiesMatcher.withUserId("user".getBytes(StandardCharsets.UTF_8));
+            propertiesMatcher.withTo("the-management");
+            propertiesMatcher.withSubject("amqp");
+            propertiesMatcher.withReplyTo("the-minions");
+            propertiesMatcher.withCorrelationId("abc");
+            propertiesMatcher.withContentEncoding("application/json");
+            propertiesMatcher.withContentType("gzip");
+            propertiesMatcher.withAbsoluteExpiryTime(123);
+            propertiesMatcher.withCreationTime(1);
+            propertiesMatcher.withGroupId("disgruntled");
+            propertiesMatcher.withGroupSequence(8192);
+            propertiesMatcher.withReplyToGroupId("/dev/null");
+            DeliveryAnnotationsMatcher daMatcher = new DeliveryAnnotationsMatcher(true);
+            daMatcher.withEntry("da1", Matchers.equalTo(1));
+            daMatcher.withEntry("da2", Matchers.equalTo(2));
+            daMatcher.withEntry("da3", Matchers.equalTo(3));
+            MessageAnnotationsMatcher maMatcher = new MessageAnnotationsMatcher(true);
+            maMatcher.withEntry("ma1", Matchers.equalTo(1));
+            maMatcher.withEntry("ma2", Matchers.equalTo(2));
+            maMatcher.withEntry("ma3", Matchers.equalTo(3));
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("ap1", Matchers.equalTo(1));
+            apMatcher.withEntry("ap2", Matchers.equalTo(2));
+            apMatcher.withEntry("ap3", Matchers.equalTo(3));
+            EncodedDataMatcher bodyMatcher = new EncodedDataMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setDeliveryAnnotationsMatcher(daMatcher);
+            payloadMatcher.setMessageAnnotationsMatcher(maMatcher);
+            payloadMatcher.setPropertiesMatcher(propertiesMatcher);
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).withMore(false).accept();
+
+            // Populate all Header values
+            message.durable(true);
+            assertEquals(true, message.durable());
+            message.priority((byte) 1);
+            assertEquals(1, message.priority());
+            message.timeToLive(65535);
+            assertEquals(65535, message.timeToLive());
+            message.firstAcquirer(true);
+            assertTrue(message.firstAcquirer());
+            message.deliveryCount(2);
+            assertEquals(2, message.deliveryCount());
+            // Populate message annotations
+            assertFalse(message.hasAnnotations());
+            assertFalse(message.hasAnnotation("ma1"));
+            message.annotation("ma1", 1);
+            assertTrue(message.hasAnnotation("ma1"));
+            assertEquals(1, message.annotation("ma1"));
+            message.annotation("ma2", 2);
+            assertEquals(2, message.annotation("ma2"));
+            message.annotation("ma3", 3);
+            assertEquals(3, message.annotation("ma3"));
+            assertTrue(message.hasAnnotations());
+            // Populate all Properties values
+            message.messageId("ID:12345");
+            assertEquals("ID:12345", message.messageId());
+            message.userId("user".getBytes(StandardCharsets.UTF_8));
+            assertArrayEquals("user".getBytes(StandardCharsets.UTF_8), message.userId());
+            message.to("the-management");
+            assertEquals("the-management", message.to());
+            message.subject("amqp");
+            assertEquals("amqp", message.subject());
+            message.replyTo("the-minions");
+            assertEquals("the-minions", message.replyTo());
+            message.correlationId("abc");
+            assertEquals("abc", message.correlationId());
+            message.contentEncoding("application/json");
+            assertEquals("application/json", message.contentEncoding());
+            message.contentType("gzip");
+            assertEquals("gzip", message.contentType());
+            message.absoluteExpiryTime(123);
+            assertEquals(123, message.absoluteExpiryTime());
+            message.creationTime(1);
+            assertEquals(1, message.creationTime());
+            message.groupId("disgruntled");
+            assertEquals("disgruntled", message.groupId());
+            message.groupSequence(8192);
+            assertEquals(8192, message.groupSequence());
+            message.replyToGroupId("/dev/null");
+            assertEquals("/dev/null", message.replyToGroupId());
+            // Populate message application properties
+            assertFalse(message.hasProperties());
+            assertFalse(message.hasProperty("ma1"));
+            message.property("ap1", 1);
+            assertEquals(1, message.property("ap1"));
+            assertTrue(message.hasProperty("ap1"));
+            message.property("ap2", 2);
+            assertEquals(2, message.property("ap2"));
+            message.property("ap3", 3);
+            assertEquals(3, message.property("ap3"));
+            assertTrue(message.hasProperties());
+
+            OutputStream stream = message.body();
+
+            stream.write(payload);
+            stream.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertNotNull(message.tracker());
+            assertNotNull(message.tracker().settlementFuture().isDone());
+            assertNotNull(message.tracker().settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamSenderWritesFooterAfterStreamClosed() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            final byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+            // First frame should include only the bits up to the body
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("ap1", Matchers.equalTo(1));
+            apMatcher.withEntry("ap2", Matchers.equalTo(2));
+            apMatcher.withEntry("ap3", Matchers.equalTo(3));
+            FooterMatcher footerMatcher = new FooterMatcher(false);
+            footerMatcher.withEntry("f1", Matchers.equalTo(1));
+            footerMatcher.withEntry("f2", Matchers.equalTo(2));
+            footerMatcher.withEntry("f3", Matchers.equalTo(3));
+            EncodedDataMatcher bodyMatcher = new EncodedDataMatcher(payload, true);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+            payloadMatcher.setFootersMatcher(footerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).withMore(false).accept();
+
+            // Populate all Header values
+            message.durable(true);
+            message.priority((byte) 1);
+            message.timeToLive(65535);
+            message.firstAcquirer(true);
+            message.deliveryCount(2);
+            // Populate message application properties
+            message.property("ap1", 1);
+            message.property("ap2", 2);
+            message.property("ap3", 3);
+            // Populate message footers
+            assertFalse(message.hasFooters());
+            assertFalse(message.hasFooter("f1"));
+            message.footer("f1", 1);
+            message.footer("f2", 2);
+            message.footer("f3", 3);
+            assertTrue(message.hasFooter("f1"));
+            assertTrue(message.hasFooters());
+
+            OutputStreamOptions bodyOptions = new OutputStreamOptions().completeSendOnClose(true);
+            OutputStream stream = message.body(bodyOptions);
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> message.encode(Collections.emptyMap()));
+
+            stream.write(payload);
+            stream.close();
+
+            assertThrows(ClientIllegalStateException.class, () -> message.footer(new Footer(Collections.emptyMap())));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertNotNull(message.tracker());
+            assertNotNull(message.tracker().settlementFuture().isDone());
+            assertNotNull(message.tracker().settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamSenderWritesFooterAfterMessageCompleted() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            final byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+            // First frame should include only the bits up to the body
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            ApplicationPropertiesMatcher apMatcher = new ApplicationPropertiesMatcher(true);
+            apMatcher.withEntry("ap1", Matchers.equalTo(1));
+            apMatcher.withEntry("ap2", Matchers.equalTo(2));
+            apMatcher.withEntry("ap3", Matchers.equalTo(3));
+            EncodedDataMatcher bodyMatcher = new EncodedDataMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setApplicationPropertiesMatcher(apMatcher);
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            // Second Frame should contains the appended footers
+            FooterMatcher footerMatcher = new FooterMatcher(false);
+            footerMatcher.withEntry("f1", Matchers.equalTo(1));
+            footerMatcher.withEntry("f2", Matchers.equalTo(2));
+            footerMatcher.withEntry("f3", Matchers.equalTo(3));
+            TransferPayloadCompositeMatcher payloadFooterMatcher = new TransferPayloadCompositeMatcher();
+            payloadFooterMatcher.setFootersMatcher(footerMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).withMore(true);
+            peer.expectTransfer().withPayload(payloadFooterMatcher).withMore(false).accept();
+
+            // Populate all Header values
+            message.durable(true);
+            message.priority((byte) 1);
+            message.timeToLive(65535);
+            message.firstAcquirer(true);
+            message.deliveryCount(2);
+            // Populate message application properties
+            message.property("ap1", 1);
+            message.property("ap2", 2);
+            message.property("ap3", 3);
+
+            OutputStreamOptions bodyOptions = new OutputStreamOptions().completeSendOnClose(false);
+            OutputStream stream = message.body(bodyOptions);
+
+            stream.write(payload);
+            stream.close();
+
+            // Populate message footers
+            message.footer("f1", 1);
+            message.footer("f2", 2);
+            message.footer("f3", 3);
+
+            message.complete();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertNotNull(message.tracker());
+            assertNotNull(message.tracker().settlementFuture().isDone());
+            assertNotNull(message.tracker().settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testAutoFlushDuringMessageSendThatExceedConfiguredBufferLimitSessionCreditLimitOnTransfer() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().maxFrameSize(1024);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            StreamSender sender = connection.openStreamSender("test-queue");
+
+            final byte[] payload = new byte[4800];
+            Arrays.fill(payload, (byte) 1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("send failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(1).withLinkCredit(10).now();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(5).withLinkCredit(10).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).accept();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testConcurrentMessageSendOnlyBlocksForInitialSendInProgress() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            sender.openFuture().get();
+
+            // Ensure that sender gets its flow before the sends are triggered.
+            connection.openReceiver("test-queue").openFuture().get();
+
+            final byte[] payload = new byte[1024];
+            Arrays.fill(payload, (byte) 1);
+
+            // One should block on the send waiting for the others send to finish
+            // otherwise they should not care about concurrency of sends.
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 2 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                } catch (Exception e) {
+                    LOG.info("Test send 2 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testConcurrentMessageSendsBlocksBehindSendWaitingForCredit() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+
+            final byte[] payload = new byte[1024];
+            Arrays.fill(payload, (byte) 1);
+
+            final CountDownLatch send1Started = new CountDownLatch(1);
+            final CountDownLatch send2Completed = new CountDownLatch(1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    ForkJoinPool.commonPool().execute(() -> send1Started.countDown());
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    assertTrue(send1Started.await(10, TimeUnit.SECONDS));
+                    LOG.info("Test send 2 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                    send2Completed.countDown();
+                } catch (Exception e) {
+                    LOG.info("Test send 2 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(0).withNextIncomingId(1).withLinkCredit(1).now();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(1).withNextIncomingId(2).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+
+            assertTrue(send2Completed.await(10, TimeUnit.SECONDS));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testConcurrentMessageSendWaitingOnSplitFramedSendToCompleteIsSentAfterCreditUpdated() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().maxFrameSize(1024);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            StreamSender sender = connection.openStreamSender("test-queue");
+
+            final byte[] payload = new byte[1536];
+            Arrays.fill(payload, (byte) 1);
+
+            final CountDownLatch send1Started = new CountDownLatch(1);
+            final CountDownLatch send2Completed = new CountDownLatch(1);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    ForkJoinPool.commonPool().execute(() -> send1Started.countDown());
+                    sender.send(Message.create(payload));
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    assertTrue(send1Started.await(10, TimeUnit.SECONDS));
+                    LOG.info("Test send 2 is preparing to fire:");
+                    Tracker tracker = sender.send(Message.create(payload));
+                    tracker.awaitSettlement(10, TimeUnit.SECONDS);
+                    send2Completed.countDown();
+                } catch (Exception e) {
+                    LOG.info("Test send 2 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(0).withNextIncomingId(1).withLinkCredit(1).now();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(0).withNextIncomingId(2).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(1).withNextIncomingId(3).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(true);
+            peer.remoteFlow().withIncomingWindow(1).withDeliveryCount(1).withNextIncomingId(4).withLinkCredit(1).queue();
+            peer.expectTransfer().withNonNullPayload().withMore(false).respond().withSettled(true).withState().accepted();
+
+            assertTrue(send2Completed.await(10, TimeUnit.SECONDS));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testMessageSendWhileStreamSendIsOpenShouldBlock() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+            final byte[] payload = new byte[1536];
+            Arrays.fill(payload, (byte) 1);
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+            OutputStreamOptions options = new OutputStreamOptions().bodyLength(8192).completeSendOnClose(false);
+            OutputStream stream = message.body(options);
+
+            final CountDownLatch sendStarted = new CountDownLatch(1);
+            final CountDownLatch sendCompleted = new CountDownLatch(1);
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    sendStarted.countDown();
+                    sender.send(Message.create(payload));
+                    sendCompleted.countDown();
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                }
+            });
+
+            EncodedDataMatcher bodyMatcher = new EncodedDataMatcher(payload);
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setMessageContentMatcher(bodyMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            assertTrue(sendStarted.await(10, TimeUnit.SECONDS));
+
+            // This should abort the streamed send as we provided a size for the body.
+            stream.close();
+            assertTrue(message.aborted());
+            assertTrue(sendCompleted.await(100, TimeUnit.SECONDS));
+            assertThrows(ClientIllegalStateException.class, () -> message.rawOutputStream());
+            assertThrows(ClientIllegalStateException.class, () -> message.body());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStreamSenderSessionCannotCreateNewResources() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openReceiver("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openReceiver("test", new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openDurableReceiver("test", "test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openDurableReceiver("test", "test", new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openDynamicReceiver());
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openDynamicReceiver(new HashMap<>()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openDynamicReceiver(new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openDynamicReceiver(new HashMap<>(), new ReceiverOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openSender("test"));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openSender("test", new SenderOptions()));
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openAnonymousSender());
+            assertThrows(ClientUnsupportedOperationException.class, () -> sender.session().openAnonymousSender(new SenderOptions()));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            sender.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageWaitingOnCreditWritesWhileCompleteSendWaitsInQueue() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+            OutputStream stream = tracker.body();
+
+            final byte[] payload1 = new byte[256];
+            Arrays.fill(payload1, (byte) 1);
+            final byte[] payload2 = new byte[256];
+            Arrays.fill(payload2, (byte) 2);
+            final byte[] payload3 = new byte[256];
+            Arrays.fill(payload3, (byte) 3);
+
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(payload1);
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(payload2);
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            EncodedDataMatcher dataMatcher3 = new EncodedDataMatcher(payload3);
+            TransferPayloadCompositeMatcher payloadMatcher3 = new TransferPayloadCompositeMatcher();
+            payloadMatcher3.setMessageContentMatcher(dataMatcher3);
+
+            final AtomicBoolean sendFailed = new AtomicBoolean();
+            final CountDownLatch streamSend1Complete = new CountDownLatch(1);
+            // Stream won't output until some body bytes are written.
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    stream.write(payload1);
+                    stream.flush();
+                } catch (IOException e) {
+                    LOG.info("send failed with error: ", e);
+                    sendFailed.set(true);
+                } finally {
+                    streamSend1Complete.countDown();
+                }
+            });
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            // Now trigger the next send by granting credit for payload 1
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(1).withLinkCredit(10).now();
+
+            assertTrue(streamSend1Complete.await(5, TimeUnit.SECONDS), "Stream sender completed first send");
+            assertFalse(sendFailed.get());
+
+            final CountDownLatch sendStarted = new CountDownLatch(1);
+            final CountDownLatch sendCompleted = new CountDownLatch(1);
+
+            ForkJoinPool.commonPool().execute(() -> {
+                try {
+                    LOG.info("Test send 1 is preparing to fire:");
+                    sendStarted.countDown();
+                    sender.send(Message.create(payload3));
+                } catch (Exception e) {
+                    LOG.info("Test send 1 failed with error: ", e);
+                    sendFailed.set(true);
+                } finally {
+                    sendCompleted.countDown();
+                }
+            });
+
+            assertTrue(sendStarted.await(10, TimeUnit.SECONDS));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            // Queue a flow that will allow send by granting credit for payload 3 via sender.send
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).queue();
+            // Now trigger the next send by granting credit for payload 2
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).now();
+
+            stream.write(payload2);
+            stream.flush();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).queue();
+            peer.expectTransfer().withPayload(payloadMatcher3).withMore(false);
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            stream.close();
+
+            assertTrue(sendCompleted.await(100, TimeUnit.SECONDS));
+            assertFalse(sendFailed.get());
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testWriteToCreditLimitFramesOfMessagePayloadOneBytePerWrite() throws Exception {
+        final int WRITE_COUNT = 10;
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withIncomingWindow(WRITE_COUNT).withNextIncomingId(1).withLinkCredit(WRITE_COUNT).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+            OutputStream stream = tracker.body();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            final byte[][] payloads = new byte[WRITE_COUNT][256];
+            for (int i = 0; i < WRITE_COUNT; ++i) {
+                payloads[i] = new byte[256];
+                Arrays.fill(payloads[i], (byte)(i + 1));
+            }
+
+            for (int i = 0; i < WRITE_COUNT; ++i) {
+                EncodedDataMatcher dataMatcher = new EncodedDataMatcher(payloads[i]);
+                TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+                payloadMatcher.setMessageContentMatcher(dataMatcher);
+
+                peer.expectTransfer().withPayload(payloadMatcher).withMore(true);
+            }
+
+            for (int i = 0; i < WRITE_COUNT; ++i) {
+                for (byte value : payloads[i]) {
+                    stream.write(value);
+                }
+                stream.flush();
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // grant one more credit for the complete to arrive.
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(WRITE_COUNT + 1).withLinkCredit(1).now();
+
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testWriteToCreditLimitFramesOfMessagePayload() throws Exception {
+        final int WRITE_COUNT = 10;
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withIncomingWindow(WRITE_COUNT).withNextIncomingId(1).withLinkCredit(WRITE_COUNT).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage tracker = sender.beginMessage();
+            OutputStream stream = tracker.body();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            final byte[][] payloads = new byte[WRITE_COUNT][256];
+            for (int i = 0; i < WRITE_COUNT; ++i) {
+                payloads[i] = new byte[256];
+                Arrays.fill(payloads[i], (byte)(i + 1));
+            }
+
+            for (int i = 0; i < WRITE_COUNT; ++i) {
+                EncodedDataMatcher dataMatcher = new EncodedDataMatcher(payloads[i]);
+                TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+                payloadMatcher.setMessageContentMatcher(dataMatcher);
+
+                peer.expectTransfer().withPayload(payloadMatcher).withMore(true);
+            }
+
+            for (int i = 0; i < WRITE_COUNT; ++i) {
+                stream.write(payloads[i]);
+                stream.flush();
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withNullPayload().withMore(false).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // grant one more credit for the complete to arrive.
+            peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(WRITE_COUNT + 1).withLinkCredit(1).now();
+
+            stream.close();
+
+            sender.closeAsync().get();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageFlushFailsAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            OutputStream stream = message.body();
+
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(new byte[] { 4, 5, 6, 7 });
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            peer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            peer.dropAfterLastHandler();
+
+            // Write two then after connection drops the message should fail on future writes
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.write(new byte[] { 4, 5, 6, 7 });
+            stream.flush();
+
+            peer.waitForScriptToComplete();
+
+            // Next write should fail as connection should have dropped.
+            stream.write(new byte[] { 8, 9, 10, 11 });
+
+            try {
+                stream.flush();
+                fail("Should not be able to flush after connection drop");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageCloseThatFlushesFailsAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            OutputStream stream = message.body();
+
+            EncodedDataMatcher dataMatcher1 = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher1 = new TransferPayloadCompositeMatcher();
+            payloadMatcher1.setMessageContentMatcher(dataMatcher1);
+
+            EncodedDataMatcher dataMatcher2 = new EncodedDataMatcher(new byte[] { 4, 5, 6, 7 });
+            TransferPayloadCompositeMatcher payloadMatcher2 = new TransferPayloadCompositeMatcher();
+            payloadMatcher2.setMessageContentMatcher(dataMatcher2);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withPayload(payloadMatcher1).withMore(true);
+            peer.expectTransfer().withPayload(payloadMatcher2).withMore(true);
+            peer.dropAfterLastHandler();
+
+            // Write two then after connection drops the message should fail on future writes
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.write(new byte[] { 4, 5, 6, 7 });
+            stream.flush();
+
+            peer.waitForScriptToComplete();
+
+            // Next write should fail as connection should have dropped.
+            stream.write(new byte[] { 8, 9, 10, 11 });
+
+            try {
+                stream.close();
+                fail("Should not be able to close after connection drop");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageWriteThatFlushesFailsAfterConnectionDropped() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.dropAfterLastHandler();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSenderOptions options = new StreamSenderOptions().writeBufferSize(1024);
+            StreamSender sender = connection.openStreamSender("test-queue", options);
+            StreamSenderMessage message = sender.beginMessage();
+
+            byte[] payload = new byte[65535];
+            Arrays.fill(payload, (byte) 65);
+            OutputStreamOptions streamOptions = new OutputStreamOptions().bodyLength(65535);
+            OutputStream stream = message.body(streamOptions);
+
+            peer.waitForScriptToComplete();
+
+            try {
+                stream.write(payload);
+                fail("Should not be able to write section after connection drop");
+            } catch (IOException ioe) {
+                assertTrue(ioe.getCause() instanceof ClientException);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamMessageSendFromByteArrayInputStreamWithoutBodySizeSet() throws Exception {
+        doTestStreamMessageSendFromByteArrayInputStream(false);
+    }
+
+    @Test
+    void testStreamMessageSendFromByteArrayInputStreamWithBodySizeSet() throws Exception {
+        doTestStreamMessageSendFromByteArrayInputStream(false);
+    }
+
+    private void doTestStreamMessageSendFromByteArrayInputStream(boolean setBodySize) throws Exception {
+        final Random random = new Random(System.nanoTime());
+        final byte[] array = new byte[4096];
+        final ByteArrayInputStream bytesIn = new ByteArrayInputStream(array);
+
+        // Populate the array with something other than zeros.
+        random.nextBytes(array);
+
+        EncodedCompositingDataSectionMatcher matcher = new EncodedCompositingDataSectionMatcher(array);
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(100).queue();
+            for (int i = 0; i < (array.length / 1023); ++i) {
+                peer.expectTransfer().withDeliveryId(0)
+                                     .withMore(true)
+                                     .withPayload(matcher);
+            }
+            // A small number of trailing bytes will be transmitted in the final frame.
+            peer.expectTransfer().withDeliveryId(0)
+                                 .withMore(false)
+                                 .withPayload(matcher);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSenderOptions options = new StreamSenderOptions().writeBufferSize(1023);
+            StreamSender sender = connection.openStreamSender("test-queue", options);
+            StreamSenderMessage tracker = sender.beginMessage();
+
+            final OutputStream stream;
+
+            if (setBodySize) {
+                stream = tracker.body(new OutputStreamOptions().bodyLength(array.length));
+            } else {
+                stream = tracker.body();
+            }
+
+            try {
+                bytesIn.transferTo(stream);
+            } finally {
+                // Ensure any trailing bytes get written and transfer marked as done.
+                stream.close();
+            }
+
+            peer.waitForScriptToComplete();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            sender.close();
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBatchAddBodySectionsWritesEach() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectBegin().respond(); // Hidden session for stream sender
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectAttach().respond();  // Open a receiver to ensure sender link has processed
+            peer.expectFlow();              // the inbound flow frame we sent previously before send.
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Sender test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort()).openFuture().get();
+            Session session = connection.openSession().openFuture().get();
+
+            StreamSenderOptions options = new StreamSenderOptions();
+            options.deliveryMode(DeliveryMode.AT_MOST_ONCE);
+            options.writeBufferSize(Integer.MAX_VALUE);
+
+            StreamSender sender = connection.openStreamSender("test-qos", options);
+
+            // Create a custom message format send context and ensure that no early buffer writes take place
+            StreamSenderMessage message = sender.beginMessage();
+
+            assertEquals(Header.DEFAULT_PRIORITY, message.priority());
+            assertEquals(Header.DEFAULT_DELIVERY_COUNT, message.deliveryCount());
+            assertEquals(Header.DEFAULT_FIRST_ACQUIRER, message.firstAcquirer());
+            assertEquals(Header.DEFAULT_TIME_TO_LIVE, message.timeToLive());
+            assertEquals(Header.DEFAULT_DURABILITY, message.durable());
+
+            // Gates send on remote flow having been sent and received
+            session.openReceiver("dummy").openFuture().get();
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher data1Matcher = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 }, true);
+            EncodedDataMatcher data2Matcher = new EncodedDataMatcher(new byte[] { 4, 5, 6, 7 }, true);
+            EncodedDataMatcher data3Matcher = new EncodedDataMatcher(new byte[] { 8, 9, 0, 1 });
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.addMessageContentMatcher(data1Matcher);
+            payloadMatcher.addMessageContentMatcher(data2Matcher);
+            payloadMatcher.addMessageContentMatcher(data3Matcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectTransfer().withMore(false).withPayload(payloadMatcher).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            List<Section<?>> sections = new ArrayList<>(3);
+            sections.add(new Data(new byte[] { 0, 1, 2, 3 }));
+            sections.add(new Data(new byte[] { 4, 5, 6, 7 }));
+            sections.add(new Data(new byte[] { 8, 9, 0, 1 }));
+
+            message.header(header);
+            message.bodySections(sections);
+
+            message.complete();
+
+            assertEquals(message, message.complete()); // Should no-op at this point
+            assertNotNull(message.tracker().settlementFuture().isDone());
+            assertNotNull(message.tracker().settlementFuture().get().settled());
+
+            sender.closeAsync().get(10, TimeUnit.SECONDS);
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/TransactionsTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/TransactionsTest.java
new file mode 100644
index 0000000..af806f9
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/TransactionsTest.java
@@ -0,0 +1,1493 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.Delivery;
+import org.apache.qpid.protonj2.client.DeliveryState;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.OutputStreamOptions;
+import org.apache.qpid.protonj2.client.Receiver;
+import org.apache.qpid.protonj2.client.ReceiverOptions;
+import org.apache.qpid.protonj2.client.Sender;
+import org.apache.qpid.protonj2.client.Session;
+import org.apache.qpid.protonj2.client.StreamSender;
+import org.apache.qpid.protonj2.client.StreamSenderMessage;
+import org.apache.qpid.protonj2.client.Tracker;
+import org.apache.qpid.protonj2.client.exceptions.ClientConnectionRemotelyClosedException;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.exceptions.ClientIllegalStateException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionDeclarationException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionNotActiveException;
+import org.apache.qpid.protonj2.client.exceptions.ClientTransactionRolledBackException;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.client.test.Wait;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.HeaderMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedDataMatcher;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transactions.TransactionErrors;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Timeout(30)
+public class TransactionsTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TransactionsTest.class);
+
+    @Test
+    public void testCoordinatorLinkSupportedOutcomes() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().withSource().withOutcomes(Accepted.DESCRIPTOR_SYMBOL.toString(),
+                                                                     Rejected.DESCRIPTOR_SYMBOL.toString(),
+                                                                     Released.DESCRIPTOR_SYMBOL.toString(),
+                                                                     Modified.DESCRIPTOR_SYMBOL.toString()).and().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+            session.commitTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTimedOutExceptionOnBeginWithNoResponse() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().requestTimeout(50);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.beginTransaction();
+                fail("Begin should have timoued out after no response.");
+            } catch (ClientTransactionDeclarationException expected) {
+                // Expect this to time out.
+            }
+
+            try {
+                session.commitTransaction();
+                fail("Commit should have failed due to no active transaction.");
+            } catch (ClientIllegalStateException expected) {
+                // Expect this to fail since transaction not declared
+            }
+
+            try {
+                session.rollbackTransaction();
+                fail("Rollback should have failed due to no active transaction.");
+            } catch (ClientIllegalStateException expected) {
+                // Expect this to fail since transaction not declared
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTimedOutExceptionOnBeginWithNoResponseThenRecoverWithNextBegin() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare();
+            peer.expectDetach().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = new ConnectionOptions().requestTimeout(50);
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.beginTransaction();
+                fail("Begin should have timoued out after no response.");
+            } catch (ClientTransactionDeclarationException expected) {
+                // Expect this to time out.
+            }
+
+            try {
+                session.commitTransaction();
+                fail("Commit should have failed due to no active transaction.");
+            } catch (ClientIllegalStateException expected) {
+                // Expect this to fail since transaction not declared
+            }
+
+            try {
+                session.rollbackTransaction();
+                fail("Rollback should have failed due to no active transaction.");
+            } catch (ClientIllegalStateException expected) {
+                // Expect this to fail since transaction not declared
+            }
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.expectDischarge().accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            session.beginTransaction();
+            session.commitTransaction();
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnBeginWhenCoordinatorLinkRefused() throws Exception {
+        final String errorMessage = "CoordinatorLinkRefusal-breadcrumb";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().reject(true, AmqpError.NOT_IMPLEMENTED.toString(), errorMessage);
+            peer.expectDetach();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.beginTransaction();
+                fail("Begin should have failed after link closed.");
+            } catch (ClientTransactionDeclarationException expected) {
+                // Expect this to time out.
+                String message = expected.getMessage();
+                assertTrue(message.contains(errorMessage));
+            }
+
+            try {
+                session.commitTransaction();
+                fail("Commit should have failed due to no active transaction.");
+            } catch (ClientTransactionNotActiveException expected) {
+                // Expect this as the begin failed on coordinator rejected
+            }
+
+            try {
+                session.rollbackTransaction();
+                fail("Rollback should have failed due to no active transaction.");
+            } catch (ClientTransactionNotActiveException expected) {
+                // Expect this as the begin failed on coordinator rejected
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnBeginWhenCoordinatorLinkClosedAfterDeclare() throws Exception {
+        final String errorMessage = "CoordinatorLinkClosed-breadcrumb";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare();
+            peer.remoteDetach().withClosed(true)
+                               .withErrorCondition(AmqpError.NOT_IMPLEMENTED.toString(), errorMessage).queue();
+            peer.expectDetach();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.beginTransaction();
+                fail("Begin should have failed after link closed.");
+            } catch (ClientException expected) {
+                // Expect this to time out.
+                String message = expected.getMessage();
+                assertTrue(message.contains(errorMessage));
+            }
+
+            try {
+                session.commitTransaction();
+                fail("Commit should have failed due to no active transaction.");
+            } catch (ClientTransactionNotActiveException expected) {
+                // Expect this as the begin failed on coordinator close
+            }
+
+            try {
+                session.rollbackTransaction();
+                fail("Rollback should have failed due to no active transaction.");
+            } catch (ClientTransactionNotActiveException expected) {
+                // Expect this as the begin failed on coordinator close
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnBeginWhenCoordinatorLinkClosedAfterDeclareAllowsNewTransactionDeclaration() throws Exception {
+        final String errorMessage = "CoordinatorLinkClosed-breadcrumb";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare();
+            peer.remoteDetach().withClosed(true)
+                               .withErrorCondition(AmqpError.NOT_IMPLEMENTED.toString(), errorMessage).queue();
+            peer.expectDetach();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.expectDischarge().accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.beginTransaction();
+                fail("Begin should have failed after link closed.");
+            } catch (ClientException expected) {
+                // Expect this to time out.
+                String message = expected.getMessage();
+                assertTrue(message.contains(errorMessage));
+            }
+
+            // Try again and expect to return to normal state now.
+            session.beginTransaction();
+            session.commitTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnCommitWhenCoordinatorLinkClosedAfterDischargeSent() throws Exception {
+        final String errorMessage = "CoordinatorLinkClosed-breadcrumb";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.expectDischarge();
+            peer.remoteDetach().withClosed(true)
+                               .withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), errorMessage).queue();
+            peer.expectDetach();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.expectDischarge().accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            try {
+                session.commitTransaction();
+                fail("Commit should have failed after link closed.");
+            } catch (ClientTransactionRolledBackException expected) {
+                // Expect this to time out.
+                String message = expected.getMessage();
+                assertTrue(message.contains(errorMessage));
+            }
+
+            session.beginTransaction();
+            session.rollbackTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnCommitWhenCoordinatorLinkClosedAfterTxnDeclaration() throws Exception {
+        doTestExceptionOnDischargeWhenCoordinatorLinkClosedAfterTxnDeclaration(true);
+    }
+
+    @Test
+    public void testExceptionOnRollbackWhenCoordinatorLinkClosedAfterTxnDeclaration() throws Exception {
+        doTestExceptionOnDischargeWhenCoordinatorLinkClosedAfterTxnDeclaration(false);
+    }
+
+    private void doTestExceptionOnDischargeWhenCoordinatorLinkClosedAfterTxnDeclaration(boolean commit) throws Exception {
+        final String errorMessage = "CoordinatorLinkClosed-breadcrumb";
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.remoteDetachLastCoordinatorLink().withClosed(true)
+                                                  .withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), errorMessage).queue();
+            peer.expectDischarge().optional();  // No discharge if close processed before commit or rollback triggered
+            peer.expectDetach();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            if (commit) {
+                try {
+                    session.commitTransaction();
+                    fail("Commit should have failed after link closed.");
+                } catch (ClientTransactionRolledBackException expected) {
+                    // Expect this to time out.
+                    String message = expected.getMessage();
+                    assertTrue(message.contains(errorMessage));
+                }
+            } else {
+                try {
+                    session.rollbackTransaction();
+                } catch (Exception ex) {
+                    LOG.debug("Caught unexpected exception from rollback", ex);
+                    fail("Rollback should not have failed after link closed.");
+                }
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnCommitWhenCoordinatorRejectsDischarge() throws Exception {
+        final String errorMessage = "Transaction aborted due to timeout";
+        final byte[] txnId1 = new byte[] { 0, 1, 2, 3 };
+        final byte[] txnId2 = new byte[] { 1, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(4).queue();
+            peer.expectDeclare().accept(txnId1);
+            peer.expectDischarge().withFail(false)
+                                  .withTxnId(txnId1)
+                                  .reject(TransactionErrors.TRANSACTION_TIMEOUT.toString(), "Transaction aborted due to timeout");
+            peer.expectDeclare().accept(txnId2);
+            peer.expectDischarge().withFail(true).withTxnId(txnId2).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            try {
+                session.commitTransaction();
+                fail("Commit should have failed after link closed.");
+            } catch (ClientTransactionRolledBackException expected) {
+                // Expect this to time out.
+                String message = expected.getMessage();
+                assertTrue(message.contains(errorMessage));
+            }
+
+            session.beginTransaction();
+            session.rollbackTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testExceptionOnRollbackWhenCoordinatorRejectsDischarge() throws Exception {
+        final String errorMessage = "Transaction aborted due to timeout";
+        final byte[] txnId1 = new byte[] { 0, 1, 2, 3 };
+        final byte[] txnId2 = new byte[] { 1, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(4).queue();
+            peer.expectDeclare().accept(txnId1);
+            peer.expectDischarge().withFail(true)
+                                  .withTxnId(txnId1)
+                                  .reject(TransactionErrors.TRANSACTION_TIMEOUT.toString(), "Transaction aborted due to timeout");
+            peer.expectDeclare().accept(txnId2);
+            peer.expectDischarge().withFail(false).withTxnId(txnId2).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            try {
+                session.rollbackTransaction();
+                fail("Commit should have failed after link closed.");
+            } catch (ClientTransactionRolledBackException expected) {
+                // Expect this to time out.
+                String message = expected.getMessage();
+                assertTrue(message.contains(errorMessage));
+            }
+
+            session.beginTransaction();
+            session.commitTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    /**
+     * Create a transaction and then close the Session which result in the remote rolling back
+     * the transaction by default so the client doesn't manually roll it back itself.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testBeginTransactionAndClose() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBeginAndCommitTransaction() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+            session.commitTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testBeginAndRollbackTransaction() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectDischarge().withFail(true).withTxnId(txnId).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+            session.rollbackTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTransactionDeclaredDispositionWithoutTxnId() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectDeclare().accept(null);
+            peer.expectClose().withError(AmqpError.DECODE_ERROR.toString(), "The txn-id field cannot be omitted").respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            try {
+                session.beginTransaction();
+                fail("Should not complete transaction begin due to client connection failure on decode issue.");
+            } catch (ClientException ex) {
+                // expected to fail
+            }
+
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToCompleteIgnoreErrors();
+        }
+    }
+
+    @Test
+    public void testBeginAndCommitTransactions() throws Exception {
+        final byte[] txnId1 = new byte[] { 0, 1, 2, 3 };
+        final byte[] txnId2 = new byte[] { 1, 1, 2, 3 };
+        final byte[] txnId3 = new byte[] { 2, 1, 2, 3 };
+        final byte[] txnId4 = new byte[] { 3, 1, 2, 3 };
+        final byte[] txnId5 = new byte[] { 4, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(10).queue();
+            peer.expectDeclare().accept(txnId1);
+            peer.expectDischarge().withFail(false).withTxnId(txnId1).accept();
+            peer.expectDeclare().accept(txnId2);
+            peer.expectDischarge().withFail(false).withTxnId(txnId2).accept();
+            peer.expectDeclare().accept(txnId3);
+            peer.expectDischarge().withFail(false).withTxnId(txnId3).accept();
+            peer.expectDeclare().accept(txnId4);
+            peer.expectDischarge().withFail(false).withTxnId(txnId4).accept();
+            peer.expectDeclare().accept(txnId5);
+            peer.expectDischarge().withFail(false).withTxnId(txnId5).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            for (int i = 0; i < 5; ++i) {
+                LOG.info("Transaction declare and discharge cycle: {}", i);
+                session.beginTransaction();
+                session.commitTransaction();
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCannotBeginSecondTransactionWhileFirstIsActive() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            try {
+                session.beginTransaction();
+                fail("Should not be allowed to begin another transaction");
+            } catch (ClientIllegalStateException cliEx) {
+                // Expected
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessageInsideOfTransaction() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectTransfer().withHandle(0)
+                                 .withNonNullPayload()
+                                 .withState().transactional().withTxnId(txnId).and()
+                                 .respond()
+                                 .withState().transactional().withTxnId(txnId).withAccepted().and()
+                                 .withSettled(true);
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+            Sender sender = session.openSender("address").openFuture().get();
+
+            session.beginTransaction();
+
+            final Tracker tracker = sender.send(Message.create("test-message"));
+
+            assertNotNull(tracker);
+            assertNotNull(tracker.settlementFuture().get());
+            assertEquals(tracker.remoteState().getType(), DeliveryState.Type.TRANSACTIONAL,
+                         "Delivery inside transaction should have Transactional state");
+            assertNotNull(tracker.state());
+            assertEquals(tracker.state().getType(), DeliveryState.Type.TRANSACTIONAL,
+                         "Delivery inside transaction should have Transactional state: " + tracker.state().getType());
+            Wait.assertTrue("Delivery in transaction should be locally settled after response", () -> tracker.settled());
+
+            session.commitTransaction();
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessagesInsideOfUniqueTransactions() throws Exception {
+        final byte[] txnId1 = new byte[] { 0, 1, 2, 3 };
+        final byte[] txnId2 = new byte[] { 1, 1, 2, 3 };
+        final byte[] txnId3 = new byte[] { 2, 1, 2, 3 };
+        final byte[] txnId4 = new byte[] { 3, 1, 2, 3 };
+
+        final byte[][] txns = new byte[][] { txnId1, txnId2, txnId3, txnId4 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(txns.length).queue();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(txns.length * 2).queue();
+            for (int i = 0; i < txns.length; ++i) {
+                peer.expectDeclare().accept(txns[i]);
+                peer.expectTransfer().withHandle(0)
+                                     .withNonNullPayload()
+                                     .withState().transactional().withTxnId(txns[i]).and()
+                                     .respond()
+                                     .withState().transactional().withTxnId(txns[i]).withAccepted().and()
+                                     .withSettled(true);
+                peer.expectDischarge().withFail(false).withTxnId(txns[i]).accept();
+            }
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+            Sender sender = session.openSender("address").openFuture().get();
+
+            for (int i = 0; i < txns.length; ++i) {
+                session.beginTransaction();
+
+                final Tracker tracker = sender.send(Message.create("test-message-" + i));
+
+                assertNotNull(tracker);
+                assertNotNull(tracker.settlementFuture().get());
+                assertEquals(tracker.remoteState().getType(), DeliveryState.Type.TRANSACTIONAL);
+                assertNotNull(tracker.state());
+                assertEquals(tracker.state().getType(), DeliveryState.Type.TRANSACTIONAL,
+                    "Delivery inside transaction should have Transactional state: " + tracker.state().getType());
+                Wait.assertTrue("Delivery in transaction should be locally settled after response", () -> tracker.settled());
+
+                session.commitTransaction();
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveMessageInsideOfTransaction() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.start();
+
+            final URI remoteURI = peer.getServerURI();
+            final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            Receiver receiver = session.openReceiver("test-queue").openFuture().get();
+
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withSettled(true)
+                                    .withState().transactional().withTxnId(txnId).withAccepted();
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            session.beginTransaction();
+
+            Delivery delivery = receiver.receive(100, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+            Message<?> received = delivery.message();
+            assertNotNull(received);
+            assertTrue(received.body() instanceof String);
+            String value = (String) received.body();
+            assertEquals("Hello World", value);
+
+            session.commitTransaction();
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveMessageInsideOfTransactionNoAutoSettleSenderSettles() throws Exception {
+        doTestReceiveMessageInsideOfTransactionNoAutoSettle(true);
+    }
+
+    @Test
+    public void testReceiveMessageInsideOfTransactionNoAutoSettleSenderDoesNotSettle() throws Exception {
+        doTestReceiveMessageInsideOfTransactionNoAutoSettle(false);
+    }
+
+    private void doTestReceiveMessageInsideOfTransactionNoAutoSettle(boolean settle) throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.start();
+
+            final URI remoteURI = peer.getServerURI();
+            final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions options = new ReceiverOptions().autoAccept(false).autoSettle(false);
+            Receiver receiver = session.openReceiver("test-queue", options).openFuture().get();
+
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withSettled(true)
+                                    .withState().transactional().withTxnId(txnId).withAccepted();
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            session.beginTransaction();
+
+            Delivery delivery = receiver.receive(100, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+            assertFalse(delivery.settled());
+            assertNull(delivery.state());
+
+            Message<?> received = delivery.message();
+            assertNotNull(received);
+            assertTrue(received.body() instanceof String);
+            String value = (String) received.body();
+            assertEquals("Hello World", value);
+
+            // Manual Accept within the transaction, settlement is ignored.
+            delivery.disposition(DeliveryState.accepted(), settle);
+
+            session.commitTransaction();
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testReceiveMessageInsideOfTransactionButAcceptAndSettleOutside() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.start();
+
+            final URI remoteURI = peer.getServerURI();
+            final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions options = new ReceiverOptions().autoAccept(false).autoSettle(false);
+            Receiver receiver = session.openReceiver("test-queue", options).openFuture().get();
+
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectDisposition().withSettled(true).withState().accepted();
+
+            session.beginTransaction();
+
+            Delivery delivery = receiver.receive(100, TimeUnit.MILLISECONDS);
+            assertNotNull(delivery);
+            assertFalse(delivery.settled());
+            assertNull(delivery.state());
+
+            Message<?> received = delivery.message();
+            assertNotNull(received);
+            assertTrue(received.body() instanceof String);
+            String value = (String) received.body();
+            assertEquals("Hello World", value);
+
+            session.commitTransaction();
+
+            // Manual Accept outside the transaction and no auto settle or accept
+            // so no transactional enlistment.
+            delivery.disposition(DeliveryState.accepted(), true);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testTransactionCommitFailWithEmptyRejectedDisposition() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectTransfer().withHandle(0)
+                                 .withNonNullPayload()
+                                 .withState().transactional().withTxnId(txnId).and()
+                                 .respond()
+                                 .withState().transactional().withTxnId(txnId).withAccepted().and()
+                                 .withSettled(true);
+            peer.expectDischarge().withFail(false).withTxnId(txnId).reject();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+            Sender sender = session.openSender("address").openFuture().get();
+
+            session.beginTransaction();
+
+            final Tracker tracker = sender.send(Message.create("test-message"));
+            assertNotNull(tracker.settlementFuture().get());
+            assertEquals(tracker.remoteState().getType(), DeliveryState.Type.TRANSACTIONAL);
+
+            try {
+                session.commitTransaction();
+                fail("Commit should fail with Rollback exception");
+            } catch (ClientTransactionRolledBackException cliRbEx) {
+                // Expected roll back due to discharge rejection
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testDeclareTransactionAfterConnectionDrops() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.dropAfterLastHandler();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            peer.waitForScriptToComplete();
+
+            try {
+                session.beginTransaction();
+                fail("Should have failed to discharge transaction");
+            } catch (ClientException cliEx) {
+                // Expected error as connection was dropped
+                LOG.debug("Client threw error on begin after connection drop", cliEx);
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testCommitTransactionAfterConnectionDropsFollowingTxnDeclared() throws Exception {
+        dischargeTransactionAfterConnectionDropsFollowingTxnDeclared(true);
+    }
+
+    @Test
+    public void testRollbackTransactionAfterConnectionDropsFollowingTxnDeclared() throws Exception {
+        dischargeTransactionAfterConnectionDropsFollowingTxnDeclared(false);
+    }
+
+    public void dischargeTransactionAfterConnectionDropsFollowingTxnDeclared(boolean commit) throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.dropAfterLastHandler();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            peer.waitForScriptToComplete();
+
+            if (commit) {
+                try {
+                    session.commitTransaction();
+                    fail("Should have failed to commit transaction");
+                } catch (ClientException cliEx) {
+                    // Expected error as connection was dropped
+                }
+            } else {
+                try {
+                    session.rollbackTransaction();
+                } catch (ClientConnectionRemotelyClosedException cliEx) {
+                    // Can get an error if the session processes the close before the
+                    // roll back is called.  Mitigating that is tricky and still leaves
+                    // the user needing to handle error when session is actually closed
+                    // via Session.close()
+                } catch (Exception ex) {
+                    LOG.info("Caught unexpected error: {}", ex);
+                    fail("Connection drops will implicitly roll back TXN on remote");
+                }
+            }
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSendMessagesNoOpWhenTransactionInDoubt() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.remoteDetach().withClosed(true)
+                               .withErrorCondition(AmqpError.RESOURCE_DELETED.toString(), "Coordinator").queue();
+            peer.expectDetach();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession().openFuture().get();
+
+            session.beginTransaction();
+
+            // After the wait TXN should be in doubt and send should no-op
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(1).queue();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            final Sender sender = session.openSender("address").openFuture().get();
+
+            for (int i = 0; i < 10; ++i) {
+                final Tracker tracker = sender.send(Message.create("test-message-"));
+
+                assertNotNull(tracker);
+                assertNotNull(tracker.settlementFuture().get());
+                assertEquals(ClientDeliveryState.ClientAccepted.getInstance(), tracker.remoteState());
+                assertTrue(tracker.remoteSettled());
+                assertNull(tracker.state());
+                assertFalse(tracker.settled());
+                assertFalse(tracker.awaitAccepted().settled());
+                assertFalse(tracker.awaitSettlement().settled());
+                assertFalse(tracker.awaitAccepted(1, TimeUnit.SECONDS).settled());
+                assertFalse(tracker.awaitSettlement(1, TimeUnit.SECONDS).settled());
+                assertSame(sender, tracker.sender());
+
+                // These should no-op since message was never sent.
+                tracker.settle();
+                tracker.disposition(ClientDeliveryState.ClientAccepted.getInstance(), true);
+            }
+
+            try {
+                session.commitTransaction();
+                fail("Should not be able to commit as remote closed coordinator");
+            } catch (ClientTransactionRolledBackException cliTxRbEx) {
+                // Expected
+            }
+
+            session.closeAsync();
+            connection.closeAsync().get(10, TimeUnit.SECONDS);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void testStreamSenderMessageCanOperatesWithinTransaction() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofSender().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            StreamSender sender = connection.openStreamSender("test-queue");
+            StreamSenderMessage message = sender.beginMessage();
+
+            // Populate all Header values
+            Header header = new Header();
+            header.setDurable(true);
+            header.setPriority((byte) 1);
+            header.setTimeToLive(65535);
+            header.setFirstAcquirer(true);
+            header.setDeliveryCount(2);
+
+            message.header(header);
+
+            OutputStreamOptions options = new OutputStreamOptions();
+            OutputStream stream = message.body(options);
+
+            HeaderMatcher headerMatcher = new HeaderMatcher(true);
+            headerMatcher.withDurable(true);
+            headerMatcher.withPriority((byte) 1);
+            headerMatcher.withTtl(65535);
+            headerMatcher.withFirstAcquirer(true);
+            headerMatcher.withDeliveryCount(2);
+            EncodedDataMatcher dataMatcher = new EncodedDataMatcher(new byte[] { 0, 1, 2, 3 });
+            TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher();
+            payloadMatcher.setHeadersMatcher(headerMatcher);
+            payloadMatcher.setMessageContentMatcher(dataMatcher);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(5).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.expectTransfer().withHandle(0)
+                                 .withMore(true)
+                                 .withPayload(payloadMatcher)
+                                 .withState().transactional().withTxnId(txnId).and()
+                                 .respond()
+                                 .withState().transactional().withTxnId(txnId).withAccepted().and()
+                                 .withSettled(true);
+            peer.expectTransfer().withMore(false).withNullPayload();
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectDetach().respond();
+            peer.expectEnd().respond();
+            peer.expectClose().respond();
+
+            sender.session().beginTransaction();
+
+            // Stream won't output until some body bytes are written since the buffer was not
+            // filled by the header write.  Then the close will complete the stream message.
+            stream.write(new byte[] { 0, 1, 2, 3 });
+            stream.flush();
+            stream.close();
+
+            sender.session().commitTransaction();
+            sender.closeAsync().get();
+
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testAcceptAndRejectInSameTransaction() throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSASLAnonymousConnect();
+            peer.expectOpen().respond();
+            peer.expectBegin().respond();
+            peer.expectAttach().ofReceiver().respond();
+            peer.expectFlow();
+            peer.start();
+
+            final URI remoteURI = peer.getServerURI();
+            final byte[] payload = createEncodedMessage(new AmqpValue<>("Hello World"));
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort());
+            Session session = connection.openSession();
+            ReceiverOptions options = new ReceiverOptions().autoAccept(false).autoSettle(false);
+            Receiver receiver = session.openReceiver("test-queue", options).openFuture().get();
+
+            peer.expectCoordinatorAttach().respond();
+            peer.remoteFlow().withLinkCredit(2).queue();
+            peer.expectDeclare().accept(txnId);
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(0)
+                                 .withDeliveryTag(new byte[] { 1 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.remoteTransfer().withHandle(0)
+                                 .withDeliveryId(1)
+                                 .withDeliveryTag(new byte[] { 2 })
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload).queue();
+            peer.expectDisposition().withSettled(true)
+                                    .withState().transactional().withTxnId(txnId).withAccepted();
+            peer.expectDisposition().withSettled(true)
+                                    .withState().transactional().withTxnId(txnId).withReleased();
+            peer.expectDischarge().withFail(false).withTxnId(txnId).accept();
+            peer.expectDetach().respond();
+            peer.expectClose().respond();
+
+            session.beginTransaction();
+
+            final Delivery delivery1 = receiver.receive(100, TimeUnit.MILLISECONDS);
+            final Delivery delivery2 = receiver.receive(100, TimeUnit.MILLISECONDS);
+
+            assertNotNull(delivery1);
+            assertFalse(delivery1.settled());
+            assertNull(delivery1.state());
+            assertNotNull(delivery2);
+            assertFalse(delivery2.settled());
+            assertNull(delivery2.state());
+
+            delivery1.accept();
+            delivery2.release();
+
+            session.commitTransaction();
+            receiver.closeAsync();
+            connection.closeAsync().get();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/WsConnectionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/WsConnectionTest.java
new file mode 100644
index 0000000..4665200
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/WsConnectionTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.qpid.protonj2.client.Client;
+import org.apache.qpid.protonj2.client.Connection;
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test for the Connection class connecting over WebSockets
+ *
+ * TODO: Have this just extend the ConnectionTest and make both client and server use WS
+ */
+@Timeout(20)
+public class WsConnectionTest extends ConnectionTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(WsConnectionTest.class);
+
+    @Override
+    protected ProtonTestServerOptions testServerOptions() {
+        return new ProtonTestServerOptions().setUseWebSockets(true);
+    }
+
+    @Override
+    protected ConnectionOptions connectionOptions() {
+        ConnectionOptions options = new ConnectionOptions();
+        options.transportOptions().useWebSockets(true);
+
+        return options;
+    }
+
+    @Override
+    protected ConnectionOptions connectionOptions(String user, String password) {
+        ConnectionOptions options = new ConnectionOptions();
+        options.transportOptions().useWebSockets(true);
+        options.user(user);
+        options.password(password);
+
+        return options;
+    }
+
+    @Test
+    public void testWSConnectFailsDueToServerListeningOverTCP() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer(testServerOptions())) {
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("WebSocket Connect test started, peer listening on: {}", remoteURI);
+
+            Client container = Client.create();
+            ConnectionOptions options = connectionOptions();
+
+            try {
+                Connection connection = container.connect(remoteURI.getHost(), remoteURI.getPort(), options);
+                connection.openFuture().get();
+                fail("Should fail to connect");
+            } catch (ExecutionException ex) {
+                LOG.info("Connection create failed due to: ", ex);
+                assertTrue(ex.getCause() instanceof ClientException);
+            }
+
+            peer.waitForScriptToCompleteIgnoreErrors();
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/WssConnectionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/WssConnectionTest.java
new file mode 100644
index 0000000..6cbe810
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/WssConnectionTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.impl;
+
+import org.apache.qpid.protonj2.client.ConnectionOptions;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+
+public class WssConnectionTest extends SslConnectionTest {
+
+    @Override
+    protected ProtonTestServerOptions serverOptions() {
+        return new ProtonTestServerOptions().setUseWebSockets(true);
+    }
+
+    @Override
+    protected ConnectionOptions connectionOptions() {
+        ConnectionOptions options = new ConnectionOptions().sslEnabled(true);
+        options.transportOptions().useWebSockets(true);
+
+        return options;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/test/ImperativeClientTestCase.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/test/ImperativeClientTestCase.java
new file mode 100644
index 0000000..8a240b5
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/test/ImperativeClientTestCase.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class ImperativeClientTestCase {
+
+    public static final boolean IS_WINDOWS = System.getProperty("os.name", "unknown").toLowerCase().contains("windows");
+
+    private final Logger LOG = LoggerFactory.getLogger(getClass());
+
+    private final Map<String, String> propertiesSetForTest = new HashMap<String, String>();
+
+    private String testName;
+
+    /**
+     * Set a System property for duration of this test only. The tearDown will guarantee to reset the property to its
+     * previous value after the test completes.
+     *
+     * @param property
+     *            The property to set
+     * @param value
+     *            the value to set it to, if null, the property will be cleared
+     */
+    protected void setTestSystemProperty(final String property, final String value) {
+        if (!propertiesSetForTest.containsKey(property)) {
+            // Record the current value so we can revert it later.
+            propertiesSetForTest.put(property, System.getProperty(property));
+        }
+
+        if (value == null) {
+            System.clearProperty(property);
+            LOG.info("Set system property '" + property + "' to be cleared");
+        } else {
+            System.setProperty(property, value);
+            LOG.info("Set system property '" + property + "' to: '" + value + "'");
+        }
+    }
+
+    /**
+     * Restore the System property values that were set by this test run.
+     */
+    protected void revertTestSystemProperties() {
+        if (!propertiesSetForTest.isEmpty()) {
+            for (String key : propertiesSetForTest.keySet()) {
+                String value = propertiesSetForTest.get(key);
+                if (value != null) {
+                    System.setProperty(key, value);
+                    LOG.info("Reverted system property '" + key + "' to: '" + value + "'");
+                } else {
+                    System.clearProperty(key);
+                    LOG.info("Reverted system property '" + key + "' to be cleared");
+                }
+            }
+
+            propertiesSetForTest.clear();
+        }
+    }
+
+    @AfterEach
+    public void tearDown(TestInfo testInfo) throws Exception {
+        LOG.info("========== tearDown " + testInfo.getDisplayName() + " ==========");
+        revertTestSystemProperties();
+    }
+
+    @BeforeEach
+    public void setUp(TestInfo testInfo) throws Exception {
+        LOG.info("========== start " + testInfo.getDisplayName() + " ==========");
+        testName = testInfo.getDisplayName();
+    }
+
+    protected String getTestName() {
+        return testName;
+    }
+
+    protected byte[] createEncodedMessage(Section<Object> body) {
+        Encoder encoder = CodecFactory.getEncoder();
+        ProtonBuffer buffer = new ProtonByteBufferAllocator().allocate();
+        encoder.writeObject(buffer, encoder.newEncoderState(), body);
+        byte[] result = new byte[buffer.getReadableBytes()];
+        buffer.readBytes(result);
+        return result;
+    }
+
+    protected byte[] createEncodedMessage(Section<?>... body) {
+        Encoder encoder = CodecFactory.getEncoder();
+        ProtonBuffer buffer = new ProtonByteBufferAllocator().allocate();
+        for (Section<?> section : body) {
+            encoder.writeObject(buffer, encoder.newEncoderState(), section);
+        }
+        byte[] result = new byte[buffer.getReadableBytes()];
+        buffer.readBytes(result);
+        return result;
+    }
+
+    protected byte[] createEncodedMessage(Data... body) {
+        Encoder encoder = CodecFactory.getEncoder();
+        ProtonBuffer buffer = new ProtonByteBufferAllocator().allocate();
+        for (Data data : body) {
+            encoder.writeObject(buffer, encoder.newEncoderState(), data);
+        }
+        byte[] result = new byte[buffer.getReadableBytes()];
+        buffer.readBytes(result);
+        return result;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/test/Wait.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/test/Wait.java
new file mode 100644
index 0000000..e1b5891
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/test/Wait.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.test;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.TimeUnit;
+
+public class Wait {
+
+    public static final long MAX_WAIT_MILLIS = 30 * 1000;
+    public static final long SLEEP_MILLIS = 200;
+    public static final String DEFAULT_FAILURE_MESSAGE = "Expected condition was not met";
+
+    @FunctionalInterface
+    public interface Condition {
+        boolean isSatisfied() throws Exception;
+    }
+
+    public static void assertTrue(Condition condition) {
+        assertTrue(DEFAULT_FAILURE_MESSAGE, condition);
+    }
+
+    public static void assertFalse(Condition condition) throws Exception {
+        assertTrue(() -> !condition.isSatisfied());
+    }
+
+    public static void assertFalse(String failureMessage, Condition condition) {
+        assertTrue(failureMessage, () -> !condition.isSatisfied());
+    }
+
+    public static void assertFalse(String failureMessage, Condition condition, final long duration) {
+        assertTrue(failureMessage, () -> !condition.isSatisfied(), duration, SLEEP_MILLIS);
+    }
+
+    public static void assertFalse(Condition condition, final long duration, final long sleep) {
+        assertTrue(DEFAULT_FAILURE_MESSAGE, () -> !condition.isSatisfied(), duration, sleep);
+    }
+
+    public static void assertTrue(Condition condition, final long duration) {
+        assertTrue(DEFAULT_FAILURE_MESSAGE, condition, duration, SLEEP_MILLIS);
+    }
+
+    public static void assertTrue(String failureMessage, Condition condition) {
+        assertTrue(failureMessage, condition, MAX_WAIT_MILLIS);
+    }
+
+    public static void assertTrue(String failureMessage, Condition condition, final long duration) {
+        assertTrue(failureMessage, condition, duration, SLEEP_MILLIS);
+    }
+
+    public static void assertTrue(Condition condition, final long duration, final long sleep) throws Exception {
+        assertTrue(DEFAULT_FAILURE_MESSAGE, condition, duration, sleep);
+    }
+
+    public static void assertTrue(String failureMessage, Condition condition, final long duration, final long sleep) {
+        boolean result = waitFor(condition, duration, sleep);
+
+        if (!result) {
+            fail(failureMessage);
+        }
+    }
+
+    public static boolean waitFor(Condition condition) throws Exception {
+        return waitFor(condition, MAX_WAIT_MILLIS);
+    }
+
+    public static boolean waitFor(final Condition condition, final long duration) throws Exception {
+        return waitFor(condition, duration, SLEEP_MILLIS);
+    }
+
+    public static boolean waitFor(final Condition condition, final long durationMillis, final long sleepMillis) {
+        try {
+            final long expiry = System.currentTimeMillis() + durationMillis;
+            boolean conditionSatisified = condition.isSatisfied();
+
+            while (!conditionSatisified && System.currentTimeMillis() < expiry) {
+                if (sleepMillis == 0) {
+                    Thread.yield();
+                } else {
+                    TimeUnit.MILLISECONDS.sleep(sleepMillis);
+                }
+                conditionSatisified = condition.isSatisfied();
+            }
+
+            return conditionSatisified;
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyBlackHoleServer.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyBlackHoleServer.java
new file mode 100644
index 0000000..0b0c151
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyBlackHoleServer.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+
+public class NettyBlackHoleServer extends NettyServer {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NettyBlackHoleServer.class);
+
+    public NettyBlackHoleServer(TransportOptions options, SslOptions sslOptions) {
+        super(options, sslOptions);
+    }
+
+    public NettyBlackHoleServer(TransportOptions options, SslOptions sslOptions, boolean needClientAuth) {
+        super(options, sslOptions, needClientAuth);
+    }
+
+    @Override
+    protected ChannelHandler getServerHandler() {
+        return new BlackHoleInboundHandler();
+    }
+
+    private class BlackHoleInboundHandler extends ChannelInboundHandlerAdapter  {
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object msg) {
+            LOG.trace("BlackHoleInboundHandler: Channel read, dropping: {}", msg);
+        }
+    }
+}
\ No newline at end of file
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyEchoServer.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyEchoServer.java
new file mode 100644
index 0000000..90c9e4d
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyEchoServer.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+
+/**
+ * Simple Netty Server used to echo all data.
+ */
+public class NettyEchoServer extends NettyServer {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NettyEchoServer.class);
+
+    public NettyEchoServer(TransportOptions options, SslOptions sslOptions, boolean needClientAuth) {
+        super(options, sslOptions, needClientAuth);
+    }
+
+    @Override
+    protected ChannelHandler getServerHandler() {
+        return new EchoServerHandler();
+    }
+
+    private class EchoServerHandler extends SimpleChannelInboundHandler<ByteBuf>  {
+
+        @Override
+        public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
+            LOG.trace("Channel read: {}", msg);
+            ctx.write(msg.copy());
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyIOContextTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyIOContextTest.java
new file mode 100644
index 0000000..0d63eda
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyIOContextTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.junit.jupiter.api.Test;
+
+class NettyIOContextTest {
+
+    @Test
+    void testCannotCreateNewTransportFromShutdownBootStrap() {
+        NettyIOContext context = new NettyIOContext(new TransportOptions(), new SslOptions(), "test");
+
+        context.shutdown();
+
+        assertThrows(IllegalStateException.class, () -> context.newTransport());
+    }
+
+    @Test
+    void testEventLoopGroupAccessibleAfterCreate() {
+        NettyIOContext context = new NettyIOContext(new TransportOptions(), new SslOptions(), "test");
+
+        assertNotNull(context.eventLoop());
+        assertFalse(context.eventLoop().isShutdown());
+
+        context.shutdown();
+
+        assertTrue(context.eventLoop().isShutdown());
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyServer.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyServer.java
new file mode 100644
index 0000000..41bb75a
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/NettyServer.java
@@ -0,0 +1,388 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+
+/**
+ * Base Server implementation used to create Netty based server implementations for
+ * unit testing aspects of the client code.
+ */
+public abstract class NettyServer implements AutoCloseable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NettyServer.class);
+
+    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
+    static final String WEBSOCKET_PATH = "/";
+
+    private EventLoopGroup bossGroup;
+    private EventLoopGroup workerGroup;
+    private Channel serverChannel;
+    private final TransportOptions options;
+    private final SslOptions sslOptions;
+    private int serverPort;
+    private final boolean needClientAuth;
+    private int maxFrameSize = TransportOptions.DEFAULT_WEBSOCKET_MAX_FRAME_SIZE;
+    private String webSocketPath = WEBSOCKET_PATH;
+    private volatile boolean fragmentWrites;
+    private volatile SslHandler sslHandler;
+    private volatile HandshakeComplete handshakeComplete;
+    private final CountDownLatch handshakeCompletion = new CountDownLatch(1);
+
+    private final AtomicBoolean started = new AtomicBoolean();
+
+    public NettyServer(TransportOptions options, SslOptions sslOptions) {
+        this(options, sslOptions, false);
+    }
+
+    public NettyServer(TransportOptions options, SslOptions sslOptions, boolean needClientAuth) {
+        this.sslOptions = sslOptions;
+        this.options = options;
+        this.needClientAuth = needClientAuth;
+    }
+
+    public boolean isSecureServer() {
+        return sslOptions.sslEnabled();
+    }
+
+    public boolean isWebSocketServer() {
+        return options.useWebSockets();
+    }
+
+    public String getWebSocketPath() {
+        return webSocketPath;
+    }
+
+    public void setWebSocketPath(String webSocketPath) {
+        this.webSocketPath = webSocketPath;
+    }
+
+    public int getMaxFrameSize() {
+        return maxFrameSize;
+    }
+
+    public void setMaxFrameSize(int maxFrameSize) {
+        this.maxFrameSize = maxFrameSize;
+    }
+
+    public void setFragmentWrites(boolean fragmentWrites) {
+        if(!isWebSocketServer()) {
+            throw new IllegalStateException("Only applicable to WebSocket servers");
+        }
+
+        this.fragmentWrites = fragmentWrites;
+    }
+
+    public boolean isFragmentWrites() {
+        return fragmentWrites;
+    }
+
+    public boolean awaitHandshakeCompletion(long delayMs) throws InterruptedException {
+        return handshakeCompletion.await(delayMs, TimeUnit.MILLISECONDS);
+    }
+
+    public HandshakeComplete getHandshakeComplete() {
+        return handshakeComplete;
+    }
+
+    protected URI getConnectionURI() throws Exception {
+        if (!started.get()) {
+            throw new IllegalStateException("Cannot get URI of non-started server");
+        }
+
+        int port = getServerPort();
+
+        String scheme;
+        String path;
+
+        if (isWebSocketServer()) {
+            if (isSecureServer()) {
+                scheme = "amqpwss";
+            } else {
+                scheme = "amqpws";
+            }
+        } else {
+            if (isSecureServer()) {
+                scheme = "amqps";
+            } else {
+                scheme = "amqp";
+            }
+        }
+
+        if (isWebSocketServer()) {
+            path = getWebSocketPath();
+        } else {
+            path = null;
+        }
+
+        return new URI(scheme, null, "localhost", port, path, null, null);
+    }
+
+    public void start() throws Exception {
+
+        if (started.compareAndSet(false, true)) {
+
+            // Configure the server.
+            bossGroup = new NioEventLoopGroup(1);
+            workerGroup = new NioEventLoopGroup();
+
+            ServerBootstrap server = new ServerBootstrap();
+            server.group(bossGroup, workerGroup);
+            server.channel(NioServerSocketChannel.class);
+            server.option(ChannelOption.SO_BACKLOG, 100);
+            server.handler(new LoggingHandler(LogLevel.INFO));
+            server.childHandler(new ChannelInitializer<>() {
+
+                @Override
+                public void initChannel(Channel ch) throws Exception {
+                    if (isSecureServer()) {
+                        SSLContext context = SslSupport.createJdkSslContext(sslOptions);
+                        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, sslOptions);
+                        engine.setUseClientMode(false);
+                        engine.setNeedClientAuth(needClientAuth);
+                        sslHandler = new SslHandler(engine);
+                        ch.pipeline().addLast(sslHandler);
+                    }
+
+                    if (isWebSocketServer()) {
+                        ch.pipeline().addLast(new HttpServerCodec());
+                        ch.pipeline().addLast(new HttpObjectAggregator(65536));
+                        ch.pipeline().addLast(new WebSocketServerProtocolHandler(getWebSocketPath(), "amqp", true, maxFrameSize));
+                    }
+
+                    ch.pipeline().addLast(new NettyServerOutboundHandler());
+                    ch.pipeline().addLast(new NettyServerInboundHandler());
+                    ch.pipeline().addLast(getServerHandler());
+                }
+            });
+
+            // Start the server.
+            serverChannel = server.bind(getServerPort()).sync().channel();
+        }
+    }
+
+    protected abstract ChannelHandler getServerHandler();
+
+    public void stop() throws InterruptedException {
+        if (started.compareAndSet(true, false)) {
+            try {
+                LOG.info("Syncing channel close");
+                serverChannel.close().sync();
+            } catch (InterruptedException e) {
+            }
+
+            // Shut down all event loops to terminate all threads.
+            int timeout = 100;
+            LOG.trace("Shutting down boss group");
+            bossGroup.shutdownGracefully(0, timeout, TimeUnit.MILLISECONDS).awaitUninterruptibly(timeout);
+            LOG.trace("Boss group shut down");
+
+            LOG.trace("Shutting down worker group");
+            workerGroup.shutdownGracefully(0, timeout, TimeUnit.MILLISECONDS).awaitUninterruptibly(timeout);
+            LOG.trace("Worker group shut down");
+        }
+    }
+
+    @Override
+    public void close() throws InterruptedException {
+        stop();
+    }
+
+    public int getServerPort() {
+        if (serverPort == 0) {
+            ServerSocket ss = null;
+            try {
+                ss = ServerSocketFactory.getDefault().createServerSocket(0);
+                serverPort = ss.getLocalPort();
+            } catch (IOException e) { // revert back to default
+                serverPort = PORT;
+            } finally {
+                try {
+                    if (ss != null ) {
+                        ss.close();
+                    }
+                } catch (IOException e) { // ignore
+                }
+            }
+        }
+        return serverPort;
+    }
+
+    private class NettyServerOutboundHandler extends ChannelOutboundHandlerAdapter  {
+
+        @Override
+        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+            LOG.trace("NettyServerHandler: Channel write: {}", msg);
+            if (isWebSocketServer() && msg instanceof ByteBuf) {
+                if (isFragmentWrites()) {
+                    ByteBuf orig = (ByteBuf) msg;
+                    int origIndex = orig.readerIndex();
+                    int split = orig.readableBytes()/2;
+
+                    ByteBuf part1 = orig.copy(origIndex, split);
+                    LOG.trace("NettyServerHandler: Part1: {}", part1);
+                    orig.readerIndex(origIndex + split);
+                    LOG.trace("NettyServerHandler: Part2: {}", orig);
+
+                    BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false, 0, part1);
+                    ctx.writeAndFlush(frame1);
+                    ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(true, 0, orig);
+                    ctx.write(frame2, promise);
+                } else {
+                    BinaryWebSocketFrame frame = new BinaryWebSocketFrame((ByteBuf) msg);
+                    ctx.write(frame, promise);
+                }
+            } else {
+                ctx.write(msg, promise);
+            }
+        }
+    }
+
+    private class NettyServerInboundHandler extends ChannelInboundHandlerAdapter  {
+
+        @Override
+        public void userEventTriggered(ChannelHandlerContext context, Object payload) {
+            if (payload instanceof HandshakeComplete) {
+                handshakeComplete = (HandshakeComplete) payload;
+                handshakeCompletion.countDown();
+            }
+        }
+
+        @Override
+        public void channelActive(final ChannelHandlerContext ctx) {
+            LOG.info("NettyServerHandler -> New active channel: {}", ctx.channel());
+            SslHandler handler = ctx.pipeline().get(SslHandler.class);
+            if (handler != null) {
+                handler.handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {
+                    @Override
+                    public void operationComplete(Future<Channel> future) throws Exception {
+                        LOG.info("Server -> SSL handshake completed. Succeeded: {}", future.isSuccess());
+                        if (!future.isSuccess()) {
+                            ctx.close();
+                        }
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+            LOG.info("NettyServerHandler: channel has gone inactive: {}", ctx.channel());
+            ctx.close();
+        }
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object msg) {
+            LOG.trace("NettyServerHandler: Channel read: {}", msg);
+            if (msg instanceof WebSocketFrame) {
+                WebSocketFrame frame = (WebSocketFrame) msg;
+                ctx.fireChannelRead(frame.content());
+            } else if (msg instanceof FullHttpRequest) {
+                // Reject anything not on the WebSocket path
+                FullHttpRequest request = (FullHttpRequest) msg;
+                sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
+            } else {
+                // Forward anything else along to the next handler.
+                ctx.fireChannelRead(msg);
+            }
+        }
+
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx) {
+            ctx.flush();
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+            LOG.info("NettyServerHandler: NettyServerHandlerException caught on channel: {}", ctx.channel());
+            // Close the connection when an exception is raised.
+            cause.printStackTrace();
+            ctx.close();
+        }
+    }
+
+    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
+        // Generate an error page if response getStatus code is not OK (200).
+        if (response.status().code() != 200) {
+            ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), StandardCharsets.UTF_8);
+            response.content().writeBytes(buf);
+            buf.release();
+            HttpUtil.setContentLength(response, response.content().readableBytes());
+        }
+
+        // Send the response and close the connection if necessary.
+        ChannelFuture f = ctx.channel().writeAndFlush(response);
+        if (!HttpUtil.isKeepAlive(request) || response.status().code() != 200) {
+            f.addListener(ChannelFutureListener.CLOSE);
+        }
+    }
+
+    protected SslHandler getSslHandler() {
+        return sslHandler;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/OpenSslTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/OpenSslTransportTest.java
new file mode 100644
index 0000000..d7f1803
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/OpenSslTransportTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.lang.reflect.Field;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.OpenSslEngine;
+import io.netty.handler.ssl.SslHandler;
+
+/**
+ * Test basic functionality of the Netty based TCP Transport ruuing in secure mode (SSL).
+ */
+@Timeout(30)
+public class OpenSslTransportTest extends SslTransportTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(OpenSslTransportTest.class);
+
+    @Test
+    public void testConnectToServerWithOpenSSLEnabled() throws Exception {
+        doTestOpenSSLSupport(true);
+    }
+
+    @Test
+    public void testConnectToServerWithOpenSSLDisabled() throws Exception {
+        doTestOpenSSLSupport(false);
+    }
+
+    private void doTestOpenSSLSupport(boolean useOpenSSL) throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            SslOptions options = createSSLOptions();
+            options.allowNativeSSL(useOpenSSL);
+
+            Transport transport = createTransport(createTransportOptions(), options);
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost());
+            assertEquals(port, transport.getPort());
+            assertOpenSSL("Transport should be using OpenSSL", useOpenSSL, transport);
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testConnectToServerWithUserSuppliedSSLContextWorksWhenOpenSSLRequested() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            SslOptions options = new SslOptions();
+
+            options.sslEnabled(true);
+            options.keyStoreLocation(CLIENT_KEYSTORE);
+            options.keyStorePassword(PASSWORD);
+            options.trustStoreLocation(CLIENT_TRUSTSTORE);
+            options.trustStorePassword(PASSWORD);
+            options.storeType(KEYSTORE_TYPE);
+
+            SSLContext sslContext = SslSupport.createJdkSslContext(options);
+
+            options = new SslOptions();
+            options.sslEnabled(true);
+            options.verifyHost(false);
+            options.allowNativeSSL(true);
+            options.sslContextOverride(sslContext);
+
+            Transport transport = createTransport(createTransportOptions(), options);
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+            assertOpenSSL("Transport should not be using OpenSSL", false, transport);
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    private void assertOpenSSL(String message, boolean expected, Transport transport) throws Exception {
+        Field channel = null;
+        Class<?> transportType = transport.getClass();
+
+        while (transportType != null && channel == null) {
+            try {
+                channel = transportType.getDeclaredField("channel");
+            } catch (NoSuchFieldException error) {
+                transportType = transportType.getSuperclass();
+                if (Object.class.equals(transportType)) {
+                    transportType = null;
+                }
+            }
+        }
+
+        assertNotNull(channel, "Transport implementation unknown");
+
+        channel.setAccessible(true);
+
+        Channel activeChannel = (Channel) channel.get(transport) ;
+        ChannelHandler handler = activeChannel.pipeline().get("ssl");
+        assertNotNull(handler, "Channel should have an SSL Handler registered");
+        assertTrue(handler instanceof SslHandler);
+        SslHandler sslHandler = (SslHandler) handler;
+
+        if (expected) {
+            assertTrue(sslHandler.engine() instanceof OpenSslEngine, message);
+        } else {
+            assertFalse(sslHandler.engine() instanceof OpenSslEngine, message);
+        }
+    }
+
+    @Override
+    @Disabled("Can't apply keyAlias in Netty OpenSSL impl")
+    @Test
+    public void testConnectWithSpecificClientAuthKeyAlias1() throws Exception {
+        // TODO - Revert to superclass version if keyAlias becomes supported for Netty.
+    }
+
+    @Override
+    @Disabled("Can't apply keyAlias in Netty OpenSSL impl")
+    @Test
+    public void testConnectWithSpecificClientAuthKeyAlias2() throws Exception {
+        // TODO - Revert to superclass version if keyAlias becomes supported for Netty.
+    }
+
+    @Override
+    protected SslOptions createSSLOptionsIsVerify(boolean verifyHost) {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.allowNativeSSL(true);
+        options.keyStoreLocation(CLIENT_KEYSTORE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStoreLocation(CLIENT_TRUSTSTORE);
+        options.trustStorePassword(PASSWORD);
+        options.storeType(KEYSTORE_TYPE);
+        options.verifyHost(verifyHost);
+
+        return options;
+    }
+
+    @Override
+    protected SslOptions createSSLOptionsWithoutTrustStore(boolean trustAll) {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.storeType(KEYSTORE_TYPE);
+        options.allowNativeSSL(true);
+        options.trustAll(trustAll);
+
+        return options;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SecureWebSocketTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SecureWebSocketTransportTest.java
new file mode 100644
index 0000000..5aa020c
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SecureWebSocketTransportTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test the WebSocketTransport with channel level security enabled.
+ */
+public class SecureWebSocketTransportTest extends SslTransportTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SecureWebSocketTransportTest.class);
+
+    @Test
+    public void testEnsureNettyIOContextCreatesWebSocketTransport() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+            } catch (Exception e) {
+                LOG.info("Failed to connect to: {}:{} as expected.", HOSTNAME, port);
+                fail("Should not have failed to connect to the server: " + HOSTNAME + ":" + port);
+            }
+
+            assertTrue(transport instanceof WebSocketTransport);
+            assertTrue(transport.isConnected());
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Override
+    protected TransportOptions createTransportOptions() {
+        return new TransportOptions().useWebSockets(true);
+    }
+
+    @Override
+    protected TransportOptions createServerTransportOptions() {
+        return new TransportOptions().useWebSockets(true);
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SslSupportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SslSupportTest.java
new file mode 100644
index 0000000..dbb0d84
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SslSupportTest.java
@@ -0,0 +1,1020 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.security.UnrecoverableKeyException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.PooledByteBufAllocator;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.OpenSslEngine;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslHandler;
+
+/**
+ * Tests for the TransportSupport class.
+ */
+@SuppressWarnings("deprecation")
+public class SslSupportTest extends ImperativeClientTestCase {
+
+    public static final String PASSWORD = "password";
+
+    public static final String HOSTNAME = "localhost";
+
+    public static final String BROKER_JKS_KEYSTORE = "src/test/resources/broker-jks.keystore";
+    public static final String BROKER_JKS_TRUSTSTORE = "src/test/resources/broker-jks.truststore";
+    public static final String CLIENT_JKS_KEYSTORE = "src/test/resources/client-jks.keystore";
+    public static final String CLIENT_JKS_TRUSTSTORE = "src/test/resources/client-jks.truststore";
+
+    public static final String BROKER_JCEKS_KEYSTORE = "src/test/resources/broker-jceks.keystore";
+    public static final String BROKER_JCEKS_TRUSTSTORE = "src/test/resources/broker-jceks.truststore";
+    public static final String CLIENT_JCEKS_KEYSTORE = "src/test/resources/client-jceks.keystore";
+    public static final String CLIENT_JCEKS_TRUSTSTORE = "src/test/resources/client-jceks.truststore";
+
+    public static final String BROKER_PKCS12_KEYSTORE = "src/test/resources/broker-pkcs12.keystore";
+    public static final String BROKER_PKCS12_TRUSTSTORE = "src/test/resources/broker-pkcs12.truststore";
+    public static final String CLIENT_PKCS12_KEYSTORE = "src/test/resources/client-pkcs12.keystore";
+    public static final String CLIENT_PKCS12_TRUSTSTORE = "src/test/resources/client-pkcs12.truststore";
+
+    public static final String KEYSTORE_JKS_TYPE = "jks";
+    public static final String KEYSTORE_JCEKS_TYPE = "jceks";
+    public static final String KEYSTORE_PKCS12_TYPE = "pkcs12";
+
+    public static final String[] ENABLED_PROTOCOLS = new String[] { "TLSv1" };
+
+    // Currently the OpenSSL implementation cannot disable SSLv2Hello
+    public static final String[] ENABLED_OPENSSL_PROTOCOLS = new String[] { "SSLv2Hello", "TLSv1" };
+
+    private static final String ALIAS_DOES_NOT_EXIST = "alias.does.not.exist";
+    private static final String ALIAS_CA_CERT = "ca";
+
+    @Test
+    public void testLegacySslProtocolsDisabledByDefaultJDK() throws Exception {
+        SslOptions options = createJksSslOptions(null);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.contains("SSLv3"), "SSLv3 should not be enabled by default");
+        assertFalse(engineProtocols.contains("SSLv2Hello"), "SSLv2Hello should not be enabled by default");
+    }
+
+    @Test
+    public void testLegacySslProtocolsDisabledByDefaultOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions(null);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.contains("SSLv3"), "SSLv3 should not be enabled by default");
+
+        // TODO - Netty is currently unable to disable OpenSSL SSLv2Hello so we are stuck with it for now.
+        // assertFalse(engineProtocols.contains("SSLv2Hello"), "SSLv2Hello should not be enabled by default");
+    }
+
+    @Test
+    public void testCreateSslContextJksStoreJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        assertEquals("TLS", context.getProtocol());
+    }
+
+    @Test
+    public void testCreateSslContextJksStoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        // TODO There is no means currently of getting the protocol from the netty SslContext.
+        // assertEquals("TLS", context.getProtocol());
+    }
+
+    @Test
+    public void testCreateSslContextJksStoreWithConfiguredContextProtocolJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        String contextProtocol = "TLSv1.2";
+        options.contextProtocol(contextProtocol);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        assertEquals(contextProtocol, context.getProtocol());
+    }
+
+    @Test
+    public void testCreateSslContextJksStoreWithConfiguredContextProtocolOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+        String contextProtocol = "TLSv1.2";
+        options.contextProtocol(contextProtocol);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        // TODO There is no means currently of getting the protocol from the netty SslContext.
+        // assertEquals(contextProtocol, context.getProtocol());
+    }
+
+    @Test
+    public void testCreateSslContextNoKeyStorePasswordJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.keyStorePassword(null);
+
+        assertThrows(UnrecoverableKeyException.class, () -> {
+            SslSupport.createJdkSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextNoKeyStorePasswordOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+        options.keyStorePassword(null);
+
+        assertThrows(UnrecoverableKeyException.class, () -> {
+            SslSupport.createOpenSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextWrongKeyStorePasswordJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.keyStorePassword("wrong");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createJdkSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextWrongKeyStorePasswordOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+        options.keyStorePassword("wrong");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createOpenSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextBadPathToKeyStoreJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.keyStoreLocation(CLIENT_JKS_KEYSTORE + ".bad");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createJdkSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextBadPathToKeyStoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+        options.keyStoreLocation(CLIENT_JKS_KEYSTORE + ".bad");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createOpenSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextWrongTrustStorePasswordJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.trustStorePassword("wrong");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createJdkSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextWrongTrustStorePasswordOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+        options.trustStorePassword("wrong");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createOpenSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextBadPathToTrustStoreJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.trustStoreLocation(CLIENT_JKS_TRUSTSTORE + ".bad");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createJdkSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextBadPathToTrustStoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+        options.trustStoreLocation(CLIENT_JKS_TRUSTSTORE + ".bad");
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createOpenSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextJceksStoreJDK() throws Exception {
+        SslOptions options = createJceksSslOptions();
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        assertEquals("TLS", context.getProtocol());
+    }
+
+    @Test
+    public void testCreateSslContextJceksStoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJceksSslOptions();
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+        assertTrue(context.isClient());
+    }
+
+    @Test
+    public void testCreateSslContextPkcs12StoreJDK() throws Exception {
+        SslOptions options = createPkcs12SslOptions();
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        assertEquals("TLS", context.getProtocol());
+    }
+
+    @Test
+    public void testCreateSslContextPkcs12StoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createPkcs12SslOptions();
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+        assertTrue(context.isClient());
+    }
+
+    @Test
+    public void testCreateSslContextIncorrectStoreTypeJDK() throws Exception {
+        SslOptions options = createPkcs12SslOptions();
+        options.storeType(KEYSTORE_JCEKS_TYPE);
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createJdkSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslContextIncorrectStoreTypeOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createPkcs12SslOptions();
+        options.storeType(KEYSTORE_JCEKS_TYPE);
+
+        assertThrows(IOException.class, () -> {
+            SslSupport.createOpenSslContext(options);
+        });
+    }
+
+    @Test
+    public void testCreateSslEngineFromPkcs12StoreJDK() throws Exception {
+        SslOptions options = createPkcs12SslOptions();
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.isEmpty());
+    }
+
+    @Test
+    public void testCreateSslEngineFromPkcs12StoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createPkcs12SslOptions();
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.isEmpty());
+    }
+
+    @Test
+    public void testCreateSslEngineFromPkcs12StoreWithExplicitEnabledProtocolsJDK() throws Exception {
+        SslOptions options = createPkcs12SslOptions(ENABLED_PROTOCOLS);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        assertArrayEquals(ENABLED_PROTOCOLS, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromPkcs12StoreWithExplicitEnabledProtocolsOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createPkcs12SslOptions(ENABLED_PROTOCOLS);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        assertArrayEquals(ENABLED_OPENSSL_PROTOCOLS, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.isEmpty());
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions();
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.isEmpty());
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledProtocolsJDK() throws Exception {
+        SslOptions options = createJksSslOptions(ENABLED_PROTOCOLS);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        assertArrayEquals(ENABLED_PROTOCOLS, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledProtocolsOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJksSslOptions(ENABLED_PROTOCOLS);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        assertArrayEquals(ENABLED_OPENSSL_PROTOCOLS, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitDisabledProtocolsJDK() throws Exception {
+        // Discover the default enabled protocols
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createSSLEngineDirectly(options);
+        String[] protocols = directEngine.getEnabledProtocols();
+        assertTrue(protocols.length > 0, "There were no initial protocols to choose from!");
+
+        // Pull out one to disable specifically
+        String[] disabledProtocol = new String[] { protocols[protocols.length - 1] };
+        String[] trimmedProtocols = Arrays.copyOf(protocols, protocols.length - 1);
+        options.disabledProtocols(disabledProtocol);
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+
+        // verify the option took effect
+        assertNotNull(engine);
+        assertArrayEquals(trimmedProtocols, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitDisabledProtocolsOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        // Discover the default enabled protocols
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createOpenSSLEngineDirectly(options);
+        String[] protocols = directEngine.getEnabledProtocols();
+        assertTrue(protocols.length > 0, "There were no initial protocols to choose from!");
+
+        // Pull out one to disable specifically
+        String[] disabledProtocol = new String[] { protocols[protocols.length - 1] };
+        String[] trimmedProtocols = Arrays.copyOf(protocols, protocols.length - 1);
+        options.disabledProtocols(disabledProtocol);
+        SslContext context = SslSupport.createOpenSslContext(options);
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+
+        // verify the option took effect
+        assertNotNull(engine);
+        assertArrayEquals(trimmedProtocols, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledAndDisabledProtocolsJDK() throws Exception {
+        // Discover the default enabled protocols
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createSSLEngineDirectly(options);
+        String[] protocols = directEngine.getEnabledProtocols();
+        assertTrue(protocols.length > 1, "There were no initial protocols to choose from!");
+
+        // Pull out two to enable, and one to disable specifically
+        String protocol1 = protocols[0];
+        String protocol2 = protocols[1];
+        String[] enabledProtocols = new String[] { protocol1, protocol2 };
+        String[] disabledProtocol = new String[] { protocol1 };
+        String[] remainingProtocols = new String[] { protocol2 };
+        options.enabledProtocols(enabledProtocols);
+        options.disabledProtocols(disabledProtocol);
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+
+        // verify the option took effect, that the disabled protocols were removed from the enabled list.
+        assertNotNull(engine);
+        assertArrayEquals(remainingProtocols, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledAndDisabledProtocolsOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        // Discover the default enabled protocols
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createOpenSSLEngineDirectly(options);
+        String[] protocols = directEngine.getEnabledProtocols();
+        assertTrue(protocols.length > 1, "There were no initial protocols to choose from!");
+
+        // Pull out two to enable, and one to disable specifically
+        String protocol1 = protocols[0];
+        String protocol2 = protocols[1];
+        String[] enabledProtocols = new String[] { protocol1, protocol2 };
+        String[] disabledProtocol = new String[] { protocol1 };
+        String[] remainingProtocols = new String[] { protocol2 };
+        options.enabledProtocols(enabledProtocols);
+        options.disabledProtocols(disabledProtocol);
+        SslContext context = SslSupport.createOpenSslContext(options);
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+
+        // Because Netty cannot currently disable SSLv2Hello in OpenSSL we need to account for it popping up.
+        ArrayList<String> remainingProtocolsList = new ArrayList<>(Arrays.asList(remainingProtocols));
+        if (!remainingProtocolsList.contains("SSLv2Hello")) {
+            remainingProtocolsList.add(0, "SSLv2Hello");
+        }
+
+        remainingProtocols = remainingProtocolsList.toArray(new String[remainingProtocolsList.size()]);
+
+        // verify the option took effect, that the disabled protocols were removed from the enabled list.
+        assertNotNull(engine);
+        assertEquals(remainingProtocolsList.size(), engine.getEnabledProtocols().length, "Enabled protocols not as expected");
+        assertTrue(remainingProtocolsList.containsAll(Arrays.asList(engine.getEnabledProtocols())), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledCiphersJDK() throws Exception {
+        // Discover the default enabled ciphers
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createSSLEngineDirectly(options);
+        String[] ciphers = directEngine.getEnabledCipherSuites();
+        assertTrue(ciphers.length > 0, "There were no initial ciphers to choose from!");
+
+        // Pull out one to enable specifically
+        String cipher = ciphers[0];
+        String[] enabledCipher = new String[] { cipher };
+        options.enabledCipherSuites(enabledCipher);
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+
+        // verify the option took effect
+        assertNotNull(engine);
+        assertArrayEquals(enabledCipher, engine.getEnabledCipherSuites(), "Enabled ciphers not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledCiphersOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        // Discover the default enabled ciphers
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createOpenSSLEngineDirectly(options);
+        String[] ciphers = directEngine.getEnabledCipherSuites();
+        assertTrue(ciphers.length > 0, "There were no initial ciphers to choose from!");
+
+        // Pull out one to enable specifically
+        String cipher = ciphers[0];
+        String[] enabledCipher = new String[] { cipher };
+        options.enabledCipherSuites(enabledCipher);
+        SslContext context = SslSupport.createOpenSslContext(options);
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+
+        // verify the option took effect
+        assertNotNull(engine);
+        assertArrayEquals(enabledCipher, engine.getEnabledCipherSuites(), "Enabled ciphers not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitDisabledCiphersJDK() throws Exception {
+        // Discover the default enabled ciphers
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createSSLEngineDirectly(options);
+        String[] ciphers = directEngine.getEnabledCipherSuites();
+        assertTrue(ciphers.length > 0, "There were no initial ciphers to choose from!");
+
+        // Pull out one to disable specifically
+        String[] disabledCipher = new String[] { ciphers[ciphers.length - 1] };
+        String[] trimmedCiphers = Arrays.copyOf(ciphers, ciphers.length - 1);
+        options.disabledCipherSuites(disabledCipher);
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+
+        // verify the option took effect
+        assertNotNull(engine);
+        assertArrayEquals(trimmedCiphers, engine.getEnabledCipherSuites(), "Enabled ciphers not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitDisabledCiphersOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        // Discover the default enabled ciphers
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createOpenSSLEngineDirectly(options);
+        String[] ciphers = directEngine.getEnabledCipherSuites();
+        assertTrue(ciphers.length > 0, "There were no initial ciphers to choose from!");
+
+        // Pull out one to disable specifically
+        String[] disabledCipher = new String[] { ciphers[ciphers.length - 1] };
+        String[] trimmedCiphers = Arrays.copyOf(ciphers, ciphers.length - 1);
+        options.disabledCipherSuites(disabledCipher);
+        SslContext context = SslSupport.createOpenSslContext(options);
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+
+        // verify the option took effect
+        assertNotNull(engine);
+        assertArrayEquals(trimmedCiphers, engine.getEnabledCipherSuites(), "Enabled ciphers not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledAndDisabledCiphersJDK() throws Exception {
+        // Discover the default enabled ciphers
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createSSLEngineDirectly(options);
+        String[] ciphers = directEngine.getEnabledCipherSuites();
+        assertTrue(ciphers.length > 1, "There werent enough initial ciphers to choose from!");
+
+        // Pull out two to enable, and one to disable specifically
+        String cipher1 = ciphers[0];
+        String cipher2 = ciphers[1];
+        String[] enabledCiphers = new String[] { cipher1, cipher2 };
+        String[] disabledCipher = new String[] { cipher1 };
+        String[] remainingCipher = new String[] { cipher2 };
+        options.enabledCipherSuites(enabledCiphers);
+        options.disabledCipherSuites(disabledCipher);
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+
+        // verify the option took effect, that the disabled ciphers were removed from the enabled list.
+        assertNotNull(engine);
+        assertArrayEquals(remainingCipher, engine.getEnabledCipherSuites(), "Enabled ciphers not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJksStoreWithExplicitEnabledAndDisabledCiphersOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        // Discover the default enabled ciphers
+        SslOptions options = createJksSslOptions();
+        SSLEngine directEngine = createOpenSSLEngineDirectly(options);
+        String[] ciphers = directEngine.getEnabledCipherSuites();
+        assertTrue(ciphers.length > 1, "There werent enough initial ciphers to choose from!");
+
+        // Pull out two to enable, and one to disable specifically
+        String cipher1 = ciphers[0];
+        String cipher2 = ciphers[1];
+        String[] enabledCiphers = new String[] { cipher1, cipher2 };
+        String[] disabledCipher = new String[] { cipher1 };
+        String[] remainingCipher = new String[] { cipher2 };
+        options.enabledCipherSuites(enabledCiphers);
+        options.disabledCipherSuites(disabledCipher);
+        SslContext context = SslSupport.createOpenSslContext(options);
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+
+        // verify the option took effect, that the disabled ciphers were removed from the enabled list.
+        assertNotNull(engine);
+        assertArrayEquals(remainingCipher, engine.getEnabledCipherSuites(), "Enabled ciphers not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJceksStoreJDK() throws Exception {
+        SslOptions options = createJceksSslOptions();
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.isEmpty());
+    }
+
+    @Test
+    public void testCreateSslEngineFromJceksStoreOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = createJceksSslOptions();
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+        assertFalse(engineProtocols.isEmpty());
+    }
+
+    @Test
+    public void testCreateSslEngineFromJceksStoreWithExplicitEnabledProtocolsJDK() throws Exception {
+        SslOptions options = createJceksSslOptions(ENABLED_PROTOCOLS);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        assertArrayEquals(ENABLED_PROTOCOLS, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineFromJceksStoreWithExplicitEnabledProtocolsOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        // Try and disable all but the one we really want but for now expect that this one plus SSLv2Hello
+        // is going to come back until the netty code can successfully disable them all.
+        SslOptions options = createJceksSslOptions(ENABLED_PROTOCOLS);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        assertArrayEquals(ENABLED_OPENSSL_PROTOCOLS, engine.getEnabledProtocols(), "Enabled protocols not as expected");
+    }
+
+    @Test
+    public void testCreateSslEngineWithVerifyHostJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.verifyHost(true);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        assertEquals("HTTPS", engine.getSSLParameters().getEndpointIdentificationAlgorithm());
+    }
+
+    @Test
+    public void testCreateSslEngineWithVerifyHostOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+        assumeTrue(OpenSsl.supportsHostnameValidation());
+
+        SslOptions options = createJksSslOptions();
+        options.verifyHost(true);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        assertEquals("HTTPS", engine.getSSLParameters().getEndpointIdentificationAlgorithm());
+    }
+
+    @Test
+    public void testCreateSslEngineWithoutVerifyHostJDK() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.verifyHost(false);
+
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createJdkSslEngine(null, -1, context, options);
+        assertNotNull(engine);
+
+        assertNull(engine.getSSLParameters().getEndpointIdentificationAlgorithm());
+    }
+
+    @Test
+    public void testCreateSslEngineWithoutVerifyHostOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+        assumeTrue(OpenSsl.supportsHostnameValidation());
+
+        SslOptions options = createJksSslOptions();
+        options.verifyHost(false);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        assertNotNull(context);
+
+        SSLEngine engine = SslSupport.createOpenSslEngine(PooledByteBufAllocator.DEFAULT, null, -1, context, options);
+        assertNotNull(engine);
+
+        assertNull(engine.getSSLParameters().getEndpointIdentificationAlgorithm());
+    }
+
+    @Test
+    public void testCreateSslContextWithKeyAliasWhichDoesntExist() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.keyAlias(ALIAS_DOES_NOT_EXIST);
+
+        try {
+            SslSupport.createJdkSslContext(options);
+            fail("Expected exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testCreateSslContextWithKeyAliasWhichRepresentsNonKeyEntry() throws Exception {
+        SslOptions options = createJksSslOptions();
+        options.keyAlias(ALIAS_CA_CERT);
+
+        try {
+            SslSupport.createJdkSslContext(options);
+            fail("Expected exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testIsOpenSSLPossible() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = new SslOptions();
+        options.allowNativeSSL(false);
+        assertFalse(SslSupport.isOpenSSLPossible(options));
+
+        options.allowNativeSSL(true);
+        assertTrue(SslSupport.isOpenSSLPossible(options));
+    }
+
+    @Test
+    public void testIsOpenSSLPossibleWhenHostNameVerificationConfigured() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+        assumeTrue(OpenSsl.supportsHostnameValidation());
+
+        SslOptions options = new SslOptions();
+        options.allowNativeSSL(true);
+
+        options.verifyHost(false);
+        assertTrue(SslSupport.isOpenSSLPossible(options));
+
+        options.verifyHost(true);
+        assertTrue(SslSupport.isOpenSSLPossible(options));
+    }
+
+    @Test
+    public void testIsOpenSSLPossibleWhenKeyAliasIsSpecified() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+        assumeTrue(OpenSsl.supportsHostnameValidation());
+
+        SslOptions options = new SslOptions();
+        options.allowNativeSSL(true);
+        options.keyAlias("alias");
+
+        assertFalse(SslSupport.isOpenSSLPossible(options));
+    }
+
+    @Test
+    public void testCreateSslHandlerJDK() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = new SslOptions();
+        options.sslEnabled(true);
+        options.allowNativeSSL(false);
+
+        SslHandler handler = SslSupport.createSslHandler(null, null, -1, options);
+        assertNotNull(handler);
+        assertFalse(handler.engine() instanceof OpenSslEngine);
+    }
+
+    @Test
+    public void testCreateSslHandlerOpenSSL() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = new SslOptions();
+        options.allowNativeSSL(true);
+
+        SslHandler handler = SslSupport.createSslHandler(PooledByteBufAllocator.DEFAULT, null, -1, options);
+        assertNotNull(handler);
+        assertTrue(handler.engine() instanceof OpenSslEngine);
+    }
+
+    @Test
+    public void testCreateOpenSSLEngineFailsWhenAllocatorMissing() throws Exception {
+        assumeTrue(OpenSsl.isAvailable());
+        assumeTrue(OpenSsl.supportsKeyManagerFactory());
+
+        SslOptions options = new SslOptions();
+        options.allowNativeSSL(true);
+
+        SslContext context = SslSupport.createOpenSslContext(options);
+        try {
+            SslSupport.createOpenSslEngine(null, null, -1, context, options);
+            fail("Should throw IllegalArgumentException for null allocator.");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    private SslOptions createJksSslOptions() {
+        return createJksSslOptions(null);
+    }
+
+    private SslOptions createJksSslOptions(String[] enabledProtocols) {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.keyStoreLocation(CLIENT_JKS_KEYSTORE);
+        options.trustStoreLocation(CLIENT_JKS_TRUSTSTORE);
+        options.storeType(KEYSTORE_JKS_TYPE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStorePassword(PASSWORD);
+        if (enabledProtocols != null) {
+            options.enabledProtocols(enabledProtocols);
+        }
+
+        return options;
+    }
+
+    private SslOptions createJceksSslOptions() {
+        return createJceksSslOptions(null);
+    }
+
+    private SslOptions createJceksSslOptions(String[] enabledProtocols) {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.keyStoreLocation(CLIENT_JCEKS_KEYSTORE);
+        options.trustStoreLocation(CLIENT_JCEKS_TRUSTSTORE);
+        options.storeType(KEYSTORE_JCEKS_TYPE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStorePassword(PASSWORD);
+        if (enabledProtocols != null) {
+            options.enabledProtocols(enabledProtocols);
+        }
+
+        return options;
+    }
+
+    private SslOptions createPkcs12SslOptions() {
+        return createPkcs12SslOptions(null);
+    }
+
+    private SslOptions createPkcs12SslOptions(String[] enabledProtocols) {
+        SslOptions options = new SslOptions();
+
+        options.keyStoreLocation(CLIENT_PKCS12_KEYSTORE);
+        options.trustStoreLocation(CLIENT_PKCS12_TRUSTSTORE);
+        options.storeType(KEYSTORE_PKCS12_TYPE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStorePassword(PASSWORD);
+        if (enabledProtocols != null) {
+            options.enabledProtocols(enabledProtocols);
+        }
+
+        return options;
+    }
+
+    private SSLEngine createSSLEngineDirectly(SslOptions options) throws Exception {
+        SSLContext context = SslSupport.createJdkSslContext(options);
+        SSLEngine engine = context.createSSLEngine();
+        return engine;
+    }
+
+    private SSLEngine createOpenSSLEngineDirectly(SslOptions options) throws Exception {
+        SslContext context = SslSupport.createOpenSslContext(options);
+        SSLEngine engine = context.newEngine(PooledByteBufAllocator.DEFAULT);
+        return engine;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SslTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SslTransportTest.java
new file mode 100644
index 0000000..a863f83
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/SslTransportTest.java
@@ -0,0 +1,388 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test basic functionality of the Netty based TCP Transport ruuing in secure mode (SSL).
+ */
+@Timeout(30)
+public class SslTransportTest extends TcpTransportTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SslTransportTest.class);
+
+    public static final String PASSWORD = "password";
+    public static final String SERVER_KEYSTORE = "src/test/resources/broker-jks.keystore";
+    public static final String SERVER_TRUSTSTORE = "src/test/resources/broker-jks.truststore";
+    public static final String SERVER_WRONG_HOST_KEYSTORE = "src/test/resources/broker-wrong-host-jks.keystore";
+    public static final String CLIENT_KEYSTORE = "src/test/resources/client-jks.keystore";
+    public static final String CLIENT_MULTI_KEYSTORE = "src/test/resources/client-multiple-keys-jks.keystore";
+    public static final String CLIENT_TRUSTSTORE = "src/test/resources/client-jks.truststore";
+    public static final String OTHER_CA_TRUSTSTORE = "src/test/resources/other-ca-jks.truststore";
+
+    public static final String CLIENT_KEY_ALIAS = "client";
+    public static final String CLIENT_DN = "O=Client,CN=client";
+    public static final String CLIENT2_KEY_ALIAS = "client2";
+    public static final String CLIENT2_DN = "O=Client2,CN=client2";
+
+    public static final String KEYSTORE_TYPE = "jks";
+
+    @Test
+    public void testConnectToServerWithoutTrustStoreFails() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptionsWithoutTrustStore(false));
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Connection failed to untrusted test server: {}:{}", HOSTNAME, port);
+            }
+
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+
+        logTransportErrors();
+
+        assertFalse(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testConnectToServerUsingUntrustedKeyFails() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            SslOptions sslOptions = createSSLOptions();
+
+            sslOptions.trustStoreLocation(OTHER_CA_TRUSTSTORE);
+            sslOptions.trustStorePassword(PASSWORD);
+
+            Transport transport = createTransport(createTransportOptions(), sslOptions);
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Connection failed to untrusted test server: {}:{}", HOSTNAME, port);
+            }
+
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+    }
+
+    @Test
+    public void testConnectToServerWithWrongKeyStorePasswordFails() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+            final CountDownLatch errored = new CountDownLatch(1);
+            final SslOptions sslOptions = createSSLOptions();
+
+            sslOptions.keyStoreLocation(CLIENT_KEYSTORE);
+            sslOptions.keyStorePassword("wrong");
+
+            Transport transport = createTransport(createTransportOptions(), sslOptions);
+            transport.connect(HOSTNAME, port, new NettyTransportListener(false) {
+
+                @Override
+                public void transportError(Throwable cause) {
+                    LOG.info("Transport error caught: {}", cause.getMessage(), cause);
+                    errored.countDown();
+                }
+            });
+
+            assertTrue(errored.await(5, TimeUnit.SECONDS));
+
+            try {
+                transport.awaitConnect();
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Connection failed when key store password was incorrect: {}:{}", HOSTNAME, port);
+            }
+
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+    }
+
+    @Test
+    public void testConnectToServerWithWrongTrustStorePasswordFails() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+            final CountDownLatch errored = new CountDownLatch(1);
+            final SslOptions sslOptions = createSSLOptions();
+
+            sslOptions.trustStoreLocation(CLIENT_TRUSTSTORE);
+            sslOptions.trustStorePassword("wrong");
+
+            Transport transport = createTransport(createTransportOptions(), sslOptions);
+            transport.connect(HOSTNAME, port, new NettyTransportListener(false) {
+
+                @Override
+                public void transportError(Throwable cause) {
+                    LOG.info("Transport error caught: {}", cause.getMessage(), cause);
+                    errored.countDown();
+                }
+            });
+
+            assertTrue(errored.await(5, TimeUnit.SECONDS));
+
+            try {
+                transport.awaitConnect();
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Connection failed when trust store password was incorrect: {}:{}", HOSTNAME, port);
+            }
+
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+    }
+
+    @Test
+    public void testConnectToServerClientTrustsAll() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptionsWithoutTrustStore(true));
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertTrue(transport.isSecure());
+
+            transport.close();
+        }
+
+        logTransportErrors();
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testConnectWithNeedClientAuth() throws Exception {
+        try (NettyEchoServer server = createEchoServer(true)) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertTrue(transport.isSecure());
+
+            // Verify there was a certificate sent to the server
+            assertTrue(server.getSslHandler().handshakeFuture().await(2, TimeUnit.SECONDS), "Server handshake did not complete in alotted time");
+            assertNotNull(server.getSslHandler().engine().getSession().getPeerCertificates());
+
+            transport.close();
+        }
+
+        logTransportErrors();
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testConnectWithSpecificClientAuthKeyAlias1() throws Exception {
+        doClientAuthAliasTestImpl(CLIENT_KEY_ALIAS, CLIENT_DN);
+    }
+
+    @Test
+    public void testConnectWithSpecificClientAuthKeyAlias2() throws Exception {
+        doClientAuthAliasTestImpl(CLIENT2_KEY_ALIAS, CLIENT2_DN);
+    }
+
+    private void doClientAuthAliasTestImpl(String alias, String expectedDN) throws Exception, URISyntaxException, IOException, InterruptedException {
+        try (NettyEchoServer server = createEchoServer(true)) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            SslOptions sslOptions = createSSLOptions();
+            sslOptions.keyStoreLocation(CLIENT_MULTI_KEYSTORE);
+            sslOptions.keyAlias(alias);
+
+            Transport transport = createTransport(createTransportOptions(), sslOptions);
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertTrue(transport.isSecure());
+
+            assertTrue(server.getSslHandler().handshakeFuture().await(2, TimeUnit.SECONDS), "Server handshake did not complete in alotted time");
+
+            Certificate[] peerCertificates = server.getSslHandler().engine().getSession().getPeerCertificates();
+            assertNotNull(peerCertificates);
+
+            Certificate cert = peerCertificates[0];
+            assertTrue(cert instanceof X509Certificate);
+            String dn = ((X509Certificate)cert).getSubjectX500Principal().getName();
+            assertEquals(expectedDN, dn, "Unexpected certificate DN");
+
+            transport.close();
+        }
+
+        logTransportErrors();
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testConnectToServerVerifyHost() throws Exception {
+        doConnectToServerVerifyHostTestImpl(true);
+    }
+
+    @Test
+    public void testConnectToServerNoVerifyHost() throws Exception {
+        doConnectToServerVerifyHostTestImpl(false);
+    }
+
+    private void doConnectToServerVerifyHostTestImpl(boolean verifyHost) throws Exception, URISyntaxException, IOException, InterruptedException {
+        SslOptions serverOptions = createServerSSLOptions();
+        serverOptions.keyStoreLocation(SERVER_WRONG_HOST_KEYSTORE);
+
+        try (NettyEchoServer server = createEchoServer(serverOptions)) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            SslOptions clientOptions = createSSLOptionsIsVerify(verifyHost);
+
+            if (verifyHost) {
+                assertTrue(clientOptions.verifyHost(), "Expected verifyHost to be true");
+            } else {
+                assertFalse(clientOptions.verifyHost(), "Expected verifyHost to be false");
+            }
+
+            Transport transport = createTransport(createTransportOptions(), clientOptions);
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                if (verifyHost) {
+                    fail("Should not have connected to the server: " + HOSTNAME + ":" + port);
+                }
+            } catch (Exception e) {
+                if (verifyHost) {
+                    LOG.info("Connection failed to test server: {}:{} as expected.", HOSTNAME, port);
+                } else {
+                    LOG.error("Failed to connect to test server: {}:{}" + HOSTNAME, port, e);
+                    fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+                }
+            }
+
+            assertNotNull(transport.getSslOptions());
+            assertEquals(verifyHost, transport.getSslOptions().verifyHost());
+
+            if (verifyHost) {
+                assertFalse(transport.isConnected());
+            } else {
+                assertTrue(transport.isConnected());
+            }
+
+            transport.close();
+        }
+    }
+
+    @Override
+    protected SslOptions createSSLOptions() {
+        return createSSLOptionsIsVerify(false);
+    }
+
+    protected SslOptions createSSLOptionsIsVerify(boolean verifyHost) {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.keyStoreLocation(CLIENT_KEYSTORE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStoreLocation(CLIENT_TRUSTSTORE);
+        options.trustStorePassword(PASSWORD);
+        options.storeType(KEYSTORE_TYPE);
+        options.verifyHost(verifyHost);
+
+        return options;
+    }
+
+    protected SslOptions createSSLOptionsWithoutTrustStore(boolean trustAll) {
+        SslOptions options = new SslOptions();
+
+        options.sslEnabled(true);
+        options.storeType(KEYSTORE_TYPE);
+        options.trustAll(trustAll);
+
+        return options;
+    }
+
+    @Override
+    protected SslOptions createServerSSLOptions() {
+        SslOptions options = new SslOptions();
+
+        // Run the server in JDK mode for now to validate cross compatibility
+        options.sslEnabled(true);
+        options.keyStoreLocation(SERVER_KEYSTORE);
+        options.keyStorePassword(PASSWORD);
+        options.trustStoreLocation(SERVER_TRUSTSTORE);
+        options.trustStorePassword(PASSWORD);
+        options.storeType(KEYSTORE_TYPE);
+        options.verifyHost(false);
+
+        return options;
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/TcpTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/TcpTransportTest.java
new file mode 100644
index 0000000..f04c49f
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/TcpTransportTest.java
@@ -0,0 +1,1093 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonNettyByteBuffer;
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.apache.qpid.protonj2.client.test.Wait;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.epoll.Epoll;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.kqueue.KQueue;
+import io.netty.channel.kqueue.KQueueEventLoopGroup;
+import io.netty.incubator.channel.uring.IOUring;
+import io.netty.incubator.channel.uring.IOUringEventLoopGroup;
+import io.netty.util.ResourceLeakDetector;
+import io.netty.util.ResourceLeakDetector.Level;
+
+/**
+ * Test basic functionality of the Netty based TCP transport.
+ */
+@Timeout(30)
+public class TcpTransportTest extends ImperativeClientTestCase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TcpTransportTest.class);
+
+    protected static final int SEND_BYTE_COUNT = 1024;
+    protected static final String HOSTNAME = "localhost";
+
+    protected volatile boolean transportInitialized;
+    protected volatile boolean transportConnected;
+    protected volatile boolean transportErrored;
+    protected final List<Throwable> exceptions = new ArrayList<>();
+    protected final List<ProtonBuffer> data = new ArrayList<>();
+    protected final AtomicInteger bytesRead = new AtomicInteger();
+
+    protected final TransportListener testListener = new NettyTransportListener(false);
+
+    protected NettyIOContext context;
+
+    @Override
+    @AfterEach
+    public void tearDown(TestInfo testInfo) throws Exception {
+        super.tearDown(testInfo);
+
+        if (context != null) {
+            context.shutdown();
+            context = null;
+        }
+    }
+
+    @Test
+    public void testCannotCreateWithIllegalArgs() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> new TcpTransport(null, createTransportOptions(), createSSLOptions()));
+        assertThrows(IllegalArgumentException.class, () -> new TcpTransport(new Bootstrap(), null, createSSLOptions()));
+        assertThrows(IllegalArgumentException.class, () -> new TcpTransport(new Bootstrap(), createTransportOptions(), null));
+    }
+
+    @Test
+    public void testCloseOnNeverConnectedTransport() throws Exception {
+        Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+        assertFalse(transport.isConnected());
+
+        transport.close();
+
+        assertFalse(transportInitialized);
+        assertFalse(transportConnected);
+        assertFalse(transportErrored);
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testCannotCallConnectOnClosedTransport() throws Exception {
+        Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+
+        transport.close();
+
+        assertThrows(IllegalStateException.class, () -> transport.connect("localhost", 5672, testListener));
+    }
+
+    @Test
+    public void testCreateWithBadHostOrPortThrowsIAE() throws Exception {
+        Transport transport = createTransport(createTransportOptions().defaultTcpPort(-1), createSSLOptions().defaultSslPort(-1));
+
+        try {
+            transport.connect(HOSTNAME, -1, testListener);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        try {
+            transport.connect(null, 5672, testListener);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        try {
+            transport.connect("", 5672, testListener);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+    }
+
+    @Test
+    public void testCreateWithNullOptionsThrowsIAE() throws Exception {
+        try {
+            new NettyIOContext(null, null, getTestName());
+            fail("Should have thrown NullPointerException");
+        } catch (NullPointerException npe) {
+        }
+
+        try {
+            new NettyIOContext(createTransportOptions(), null, getTestName());
+            fail("Should have thrown NullPointerException");
+        } catch (NullPointerException npe) {
+        }
+
+        try {
+            new NettyIOContext(null, createSSLOptions(), getTestName());
+            fail("Should have thrown NullPointerException");
+        } catch (NullPointerException npe) {
+        }
+    }
+
+    @Test
+    public void testConnectWithoutRunningServer() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            server.close();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+
+            assertNull(transport.getTransportListener());
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Failed to connect to: {}:{} as expected.", HOSTNAME, port);
+            }
+
+            assertEquals(testListener, transport.getTransportListener());
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+
+        assertTrue(transportInitialized);
+        assertFalse(transportConnected);
+        assertTrue(transportErrored);
+        assertFalse(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testConnectWithoutListenerFails() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+
+            try {
+                transport.connect(HOSTNAME, port, null);
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (IllegalArgumentException e) {
+                LOG.info("Failed to connect to: {}:{} as expected.", HOSTNAME, port);
+            }
+
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+    }
+
+    @Test
+    public void testConnectToServer() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+
+            final URI remoteURI = transport.getRemoteURI();
+
+            if (transport.isSecure()) {
+                if (transport.getTransportOptions().useWebSockets()) {
+                    assertEquals(remoteURI.getScheme(), "wss");
+                } else {
+                    assertEquals(remoteURI.getScheme(), "ssl");
+                }
+            } else {
+                if (transport.getTransportOptions().useWebSockets()) {
+                    assertEquals(remoteURI.getScheme(), "ws");
+                } else {
+                    assertEquals(remoteURI.getScheme(), "tcp");
+                }
+            }
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertTrue(transportInitialized);
+        assertTrue(transportConnected);
+        assertFalse(transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testMultipleConnectionsToServer() throws Exception {
+        final int CONNECTION_COUNT = 10;
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            List<Transport> transports = new ArrayList<>();
+
+            NettyIOContext context = createContext(createTransportOptions(), createSSLOptions());
+
+            for (int i = 0; i < CONNECTION_COUNT; ++i) {
+                Transport transport = context.newTransport();
+                try {
+                    transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                    assertTrue(transport.isConnected());
+                    LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+                    transports.add(transport);
+                } catch (Exception e) {
+                    fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+                }
+            }
+
+            for (Transport transport : transports) {
+                transport.close();
+            }
+        }
+
+        assertTrue(transportInitialized);
+        assertFalse(transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testMultipleConnectionsSendReceive() throws Exception {
+        final int CONNECTION_COUNT = 10;
+        final int FRAME_SIZE = 8;
+
+        ProtonNettyByteBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(FRAME_SIZE));
+        for (int i = 0; i < 8; ++i) {
+            sendBuffer.writeByte('A');
+        }
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            List<Transport> transports = new ArrayList<>();
+
+            NettyIOContext context = createContext(createTransportOptions(), createSSLOptions());
+
+            for (int i = 0; i < CONNECTION_COUNT; ++i) {
+                Transport transport = context.newTransport();
+                try {
+                    transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                    transport.writeAndFlush(sendBuffer.copy());
+                    transports.add(transport);
+                } catch (Exception e) {
+                    fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+                }
+            }
+
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    LOG.debug("Checking completion: read {} expecting {}", bytesRead.get(), (FRAME_SIZE * CONNECTION_COUNT));
+                    return bytesRead.get() == (FRAME_SIZE * CONNECTION_COUNT);
+                }
+            }, 10000, 50));
+
+            for (Transport transport : transports) {
+                transport.close();
+            }
+        }
+
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testDetectServerClose() throws Exception {
+        Transport transport = null;
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            server.close();
+        }
+
+        final Transport connectedTransport = transport;
+        assertTrue(Wait.waitFor(new Wait.Condition() {
+            @Override
+            public boolean isSatisfied() throws Exception {
+                return !connectedTransport.isConnected();
+            }
+        }, 10000, 50));
+
+        assertTrue(data.isEmpty());
+
+        try {
+            transport.close();
+        } catch (Exception ex) {
+            fail("Close of a disconnect transport should not generate errors");
+        }
+    }
+
+    @Test
+    public void testZeroSizedSentNoErrorsWriteAndFlush() throws Exception {
+        testZeroSizedSentNoErrors(true);
+    }
+
+    @Test
+    public void testZeroSizedSentNoErrorsWriteThenFlush() throws Exception {
+        testZeroSizedSentNoErrors(false);
+    }
+
+    private void testZeroSizedSentNoErrors(boolean writeAndFlush) throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            if (writeAndFlush) {
+                transport.writeAndFlush(new ProtonNettyByteBuffer(Unpooled.buffer(0)));
+            } else {
+                transport.write(new ProtonNettyByteBuffer(Unpooled.buffer(0)));
+                transport.flush();
+            }
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testUseAllocatorToCreateFixedSizeOutputBuffer() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            ProtonBuffer buffer = transport.getBufferAllocator().outputBuffer(64, 512);
+
+            assertEquals(64, buffer.capacity());
+            assertEquals(512, buffer.maxCapacity());
+
+            transport.writeAndFlush(buffer);
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testUseAllocatorToCreateFixedSizeHeapBuffer() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            ProtonBuffer buffer = transport.getBufferAllocator().allocate(64, 512);
+
+            assertEquals(64, buffer.capacity());
+            assertEquals(512, buffer.maxCapacity());
+
+            transport.writeAndFlush(buffer);
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testDataSentWithWriteAndThenFlushedIsReceived() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            ProtonBuffer sendBuffer = transport.getBufferAllocator().outputBuffer(SEND_BYTE_COUNT);
+            for (int i = 0; i < SEND_BYTE_COUNT; ++i) {
+                sendBuffer.writeByte('A');
+            }
+
+            transport.write(sendBuffer, () -> LOG.debug("Netty repprts write complete"));
+            LOG.trace("Flush of Transport happens now");
+            transport.flush();
+
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    return !data.isEmpty();
+                }
+            }, 10000, 50));
+
+            assertEquals(SEND_BYTE_COUNT, data.get(0).getReadableBytes());
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testDataSentWithWriteAndFlushIsReceived() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            ProtonBuffer sendBuffer = transport.getBufferAllocator().outputBuffer(SEND_BYTE_COUNT);
+            for (int i = 0; i < SEND_BYTE_COUNT; ++i) {
+                sendBuffer.writeByte('A');
+            }
+
+            transport.writeAndFlush(sendBuffer, () -> LOG.debug("Netty repprts write complete"));
+
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    return !data.isEmpty();
+                }
+            }, 10000, 50));
+
+            assertEquals(SEND_BYTE_COUNT, data.get(0).getReadableBytes());
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testMultipleDataPacketsSentAreReceived() throws Exception {
+        doMultipleDataPacketsSentAndReceive(SEND_BYTE_COUNT, 1);
+    }
+
+    @Test
+    public void testMultipleDataPacketsSentAreReceivedRepeatedly() throws Exception {
+        doMultipleDataPacketsSentAndReceive(SEND_BYTE_COUNT, 10);
+    }
+
+    public void doMultipleDataPacketsSentAndReceive(final int byteCount, final int iterations) throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            ProtonNettyByteBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(byteCount));
+            for (int i = 0; i < byteCount; ++i) {
+                sendBuffer.writeByte('A');
+            }
+
+            for (int i = 0; i < iterations; ++i) {
+                transport.writeAndFlush(sendBuffer.copy());
+            }
+
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    return bytesRead.get() == (byteCount * iterations);
+                }
+            }, 10000, 50));
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testSendToClosedTransportFails() throws Exception {
+        Transport transport = null;
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+
+            transport.close();
+
+            ProtonNettyByteBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(10));
+            try {
+                transport.writeAndFlush(sendBuffer);
+                fail("Should throw on send of closed transport");
+            } catch (IOException ex) {
+            }
+        }
+    }
+
+    @Test
+    public void testConnectRunsInitializationMethod() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+            final CountDownLatch initialized = new CountDownLatch(1);
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, new NettyTransportListener(false) {
+
+                    @Override
+                    public void transportInitialized(Transport transport) {
+                        initialized.countDown();
+                        assertFalse(transport.isConnected());
+                    }
+                });
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            initialized.await();
+            transport.awaitConnect();
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+            assertEquals(0, initialized.getCount());
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testFailureInInitializationRoutineFailsConnect() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions(), createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, new NettyTransportListener(false) {
+
+                    @Override
+                    public void transportInitialized(Transport transport) {
+                        throw new RuntimeException();
+                    }
+                }).awaitConnect();
+                fail("Should not have connected to the server at " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Failed to connect to: {}:{} as expected.", HOSTNAME, port);
+            }
+
+            assertFalse(transport.isConnected(), "Should not be connected");
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+
+            transport.close();
+        }
+
+        assertTrue(transportErrored);
+        assertFalse(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Disabled("Used for checking for transport level leaks, my be unstable on CI.")
+    @Test
+    public void testSendToClosedTransportFailsButDoesNotLeak() throws Exception {
+        Transport transport = null;
+
+        ResourceLeakDetector.setLevel(Level.PARANOID);
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            for (int i = 0; i < 256; ++i) {
+                transport = createTransport(createTransportOptions(), createSSLOptions());
+                try {
+                    transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                    LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+                } catch (Exception e) {
+                    fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+                }
+
+                assertTrue(transport.isConnected());
+
+                ProtonBuffer sendBuffer = transport.getBufferAllocator().outputBuffer(10 * 1024 * 1024);
+                sendBuffer.writeBytes(new byte[] {0, 1, 2, 3, 4});
+
+                transport.close();
+
+                try {
+                    transport.writeAndFlush(sendBuffer);
+                    fail("Should throw on send of closed transport");
+                } catch (IOException ex) {
+                }
+            }
+
+            System.gc();
+        }
+    }
+
+    @Test
+    public void testCreateFailsIfUnknownPerferredNativeIOLayerSelected() throws Exception {
+        TransportOptions options = createTransportOptions();
+        options.allowNativeIO(true);
+        options.nativeIOPeference("NATIVE-IO");
+
+        assertThrows(IllegalArgumentException.class, () -> createTransport(options, createSSLOptions()));
+    }
+
+    @Test
+    public void testConnectToServerWithEpollEnabled() throws Exception {
+        doTestEpollSupport(true);
+    }
+
+    @Test
+    public void testConnectToServerWithEpollDisabled() throws Exception {
+        doTestEpollSupport(false);
+    }
+
+    private void doTestEpollSupport(boolean useEpoll) throws Exception {
+        assumeTrue(Epoll.isAvailable());
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            TransportOptions options = createTransportOptions();
+            options.allowNativeIO(useEpoll);
+            options.nativeIOPeference("EPOLL");
+            Transport transport = createTransport(options, createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+            assertEpoll("Transport should be using Epoll", useEpoll, transport);
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertFalse(transportErrored);
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Disabled("Disabled until the io_uring support matures, can cause CI issues")
+    @Test
+    public void testConnectToServerWithIOUringEnabled() throws Exception {
+        doTestIORingSupport(true);
+    }
+
+    @Disabled("Disabled until the io_uring support matures, can cause CI issues")
+    @Test
+    public void testConnectToServerWithIOUringDisabled() throws Exception {
+        doTestIORingSupport(false);
+    }
+
+    private void doTestIORingSupport(boolean useIOUring) throws Exception {
+        assumeTrue(IOUring.isAvailable());
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            TransportOptions options = createTransportOptions();
+            options.allowNativeIO(useIOUring);
+            options.nativeIOPeference("IO_URING");
+            Transport transport = createTransport(options, createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+            assertIOUring("Transport should be using URing", useIOUring, transport);
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertFalse(transportErrored);
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    private void assertEpoll(String message, boolean expected, Transport transport) throws Exception {
+        Field bootstrap = null;
+        Class<?> transportType = transport.getClass();
+
+        while (transportType != null && bootstrap == null) {
+            try {
+                bootstrap = transportType.getDeclaredField("bootstrap");
+            } catch (NoSuchFieldException error) {
+                transportType = transportType.getSuperclass();
+                if (Object.class.equals(transportType)) {
+                    transportType = null;
+                }
+            }
+        }
+
+        assertNotNull(bootstrap, "Transport implementation unknown");
+
+        bootstrap.setAccessible(true);
+
+        Bootstrap transportBootstrap = (Bootstrap) bootstrap.get(transport);
+
+        if (expected) {
+            assertTrue(transportBootstrap.config().group() instanceof EpollEventLoopGroup, message);
+        } else {
+            assertFalse(transportBootstrap.config().group() instanceof EpollEventLoopGroup, message);
+        }
+    }
+
+    private void assertIOUring(String message, boolean expected, Transport transport) throws Exception {
+        Field bootstrap = null;
+        Class<?> transportType = transport.getClass();
+
+        while (transportType != null && bootstrap == null) {
+            try {
+                bootstrap = transportType.getDeclaredField("bootstrap");
+            } catch (NoSuchFieldException error) {
+                transportType = transportType.getSuperclass();
+                if (Object.class.equals(transportType)) {
+                    transportType = null;
+                }
+            }
+        }
+
+        assertNotNull(bootstrap, "Transport implementation unknown");
+
+        bootstrap.setAccessible(true);
+
+        Bootstrap transportBootstrap = (Bootstrap) bootstrap.get(transport);
+
+        if (expected) {
+            assertTrue(transportBootstrap.config().group() instanceof IOUringEventLoopGroup, message);
+        } else {
+            assertFalse(transportBootstrap.config().group() instanceof IOUringEventLoopGroup, message);
+        }
+    }
+
+    @Test
+    public void testConnectToServerWithKQueueEnabled() throws Exception {
+        doTestKQueueSupport(true);
+    }
+
+    @Test
+    public void testConnectToServerWithKQueueDisabled() throws Exception {
+        doTestKQueueSupport(false);
+    }
+
+    private void doTestKQueueSupport(boolean useKQueue) throws Exception {
+        assumeTrue(KQueue.isAvailable());
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            int port = server.getServerPort();
+
+            TransportOptions options = createTransportOptions();
+            options.allowNativeIO(true);
+            Transport transport = createTransport(options, createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+            assertKQueue("Transport should be using Kqueue", useKQueue, transport);
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    private void assertKQueue(String message, boolean expected, Transport transport) throws Exception {
+        Field group = null;
+        Class<?> transportType = transport.getClass();
+
+        while (transportType != null && group == null) {
+            try {
+                group = transportType.getDeclaredField("group");
+            } catch (NoSuchFieldException error) {
+                transportType = transportType.getSuperclass();
+                if (Object.class.equals(transportType)) {
+                    transportType = null;
+                }
+            }
+        }
+
+        assertNotNull(group, "Transport implementation unknown");
+
+        group.setAccessible(true);
+        if (expected) {
+            assertTrue(group.get(transport) instanceof KQueueEventLoopGroup, message);
+        } else {
+            assertFalse(group.get(transport) instanceof KQueueEventLoopGroup, message);
+        }
+    }
+
+    protected NettyIOContext createContext(TransportOptions options, SslOptions sslOptions) {
+        if (context != null) {
+            throw new IllegalStateException("Test already has a defined Netty IO Context");
+        }
+
+        return this.context = new NettyIOContext(options, sslOptions, getTestName());
+    }
+
+    protected TcpTransport createTransport(TransportOptions options, SslOptions sslOptions) {
+        if (context != null) {
+            throw new IllegalStateException("Test already has a defined Netty IO Context");
+        } else {
+            this.context = new NettyIOContext(options, sslOptions, getTestName());
+        }
+
+        return context.newTransport();
+    }
+
+    protected TransportOptions createTransportOptions() {
+        return new TransportOptions();
+    }
+
+    protected SslOptions createSSLOptions() {
+        return new SslOptions().sslEnabled(false);
+    }
+
+    protected TransportOptions createServerTransportOptions() {
+        return new TransportOptions();
+    }
+
+    protected SslOptions createServerSSLOptions() {
+        return new SslOptions().sslEnabled(false);
+    }
+
+    protected void logTransportErrors() {
+        if (!exceptions.isEmpty()) {
+            for(Throwable ex : exceptions) {
+                LOG.info("Transport sent exception: {}", ex, ex);
+            }
+        }
+    }
+
+    protected final NettyEchoServer createEchoServer() {
+        return createEchoServer(false);
+    }
+
+    protected final NettyEchoServer createEchoServer(SslOptions options) {
+        return createEchoServer(options, false);
+    }
+
+    protected final NettyEchoServer createEchoServer(boolean needClientAuth) {
+        return createEchoServer(createServerSSLOptions(), needClientAuth);
+    }
+
+    protected final NettyEchoServer createEchoServer(SslOptions options, boolean needClientAuth) {
+        return createEchoServer(createServerTransportOptions(), options, needClientAuth);
+    }
+
+    protected final NettyEchoServer createEchoServer(TransportOptions options, SslOptions sslOptions, boolean needClientAuth) {
+        return new NettyEchoServer(options, sslOptions, needClientAuth);
+    }
+
+    public class NettyTransportListener implements TransportListener {
+        final boolean retainDataBufs;
+
+        NettyTransportListener(boolean retainDataBufs) {
+            this.retainDataBufs = retainDataBufs;
+        }
+
+        @Override
+        public void transportRead(ProtonBuffer incoming) {
+            LOG.debug("Client has new incoming data of size: {}", incoming.getReadableBytes());
+            data.add(incoming);
+            bytesRead.addAndGet(incoming.getReadableBytes());
+
+            if (retainDataBufs) {
+                ((ByteBuf) incoming.unwrap()).retain();
+            }
+        }
+
+        @Override
+        public void transportError(Throwable cause) {
+            LOG.info("Transport error caught: {}", cause.getMessage(), cause);
+            transportErrored = true;
+            exceptions.add(cause);
+        }
+
+        @Override
+        public void transportConnected(Transport transport) {
+            transportConnected = true;
+        }
+
+        @Override
+        public void transportInitialized(Transport transport) {
+            transportInitialized = true;
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/WebSocketTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/WebSocketTransportTest.java
new file mode 100644
index 0000000..2fabf0c
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/WebSocketTransportTest.java
@@ -0,0 +1,454 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonNettyByteBuffer;
+import org.apache.qpid.protonj2.client.SslOptions;
+import org.apache.qpid.protonj2.client.TransportOptions;
+import org.apache.qpid.protonj2.client.test.Wait;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete;
+
+/**
+ * Test the Netty based WebSocket Transport
+ */
+@Timeout(30)
+public class WebSocketTransportTest extends TcpTransportTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(WebSocketTransportTest.class);
+
+    @Override
+    protected TransportOptions createTransportOptions() {
+        return new TransportOptions().useWebSockets(true);
+    }
+
+    @Override
+    protected TransportOptions createServerTransportOptions() {
+        return new TransportOptions().useWebSockets(true);
+    }
+
+    @Override
+    @Test
+    public void testCannotCreateWithIllegalArgs() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> new WebSocketTransport(null, createTransportOptions(), createSSLOptions()));
+        assertThrows(IllegalArgumentException.class, () -> new WebSocketTransport(new Bootstrap(), null, createSSLOptions()));
+        assertThrows(IllegalArgumentException.class, () -> new WebSocketTransport(new Bootstrap(), createTransportOptions(), null));
+    }
+
+    @Test
+    public void testConnectToServerUsingCorrectPath() throws Exception {
+        final String WEBSOCKET_PATH = "/testpath";
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.setWebSocketPath(WEBSOCKET_PATH);
+            server.start();
+
+            final int port = server.getServerPort();
+
+            Transport transport = createTransport(createTransportOptions().webSocketPath(WEBSOCKET_PATH), createSSLOptions());
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport instanceof WebSocketTransport);
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+
+            transport.close();
+
+            // Additional close should not fail or cause other problems.
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testConnectToServerUsingIncorrectPath() throws Exception {
+        final String WEBSOCKET_PATH = "/testpath";
+
+        try (NettyEchoServer server = createEchoServer()) {
+            // No configured path means it won't match the requested one.
+            server.start();
+
+            final int port = server.getServerPort();
+
+            server.close();
+
+            Transport transport = createTransport(createTransportOptions().webSocketPath(WEBSOCKET_PATH), createSSLOptions());
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
+            } catch (Exception e) {
+                LOG.info("Failed to connect to: {}:{} as expected.", HOSTNAME, port);
+            }
+
+            assertTrue(transport instanceof WebSocketTransport);
+            assertFalse(transport.isConnected());
+
+            transport.close();
+        }
+
+        assertTrue(transportErrored);
+        assertFalse(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    @Test
+    public void testConnectionsSendReceiveLargeDataWhenFrameSizeAllowsIt() throws Exception {
+        final int FRAME_SIZE = 8192;
+
+        ProtonBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(FRAME_SIZE));
+        for (int i = 0; i < FRAME_SIZE; ++i) {
+            sendBuffer.writeByte('A');
+        }
+
+        try (NettyEchoServer server = createEchoServer()) {
+            // Server should pass the data through without issue with this size
+            server.setMaxFrameSize(FRAME_SIZE);
+            server.start();
+
+            final int port = server.getServerPort();
+
+            List<Transport> transports = new ArrayList<>();
+
+            Transport transport = createTransport(createTransportOptions().webSocketMaxFrameSize(FRAME_SIZE), createSSLOptions());
+
+            try {
+                // The transport should allow for the size of data we sent.
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                transports.add(transport);
+                transport.writeAndFlush(sendBuffer.copy());
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport instanceof WebSocketTransport);
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    LOG.debug("Checking completion: read {} expecting {}", bytesRead.get(), FRAME_SIZE);
+                    return bytesRead.get() == FRAME_SIZE || !transport.isConnected();
+                }
+            }, 10000, 50));
+
+            assertTrue(transport.isConnected(), "Connection failed while receiving.");
+
+            transport.close();
+        }
+
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testConnectionReceivesFragmentedDataSingleWriteAndFlush() throws Exception {
+        testConnectionReceivesFragmentedData(true);
+    }
+
+    @Test
+    public void testConnectionReceivesFragmentedDataWriteThenFlush() throws Exception {
+        testConnectionReceivesFragmentedData(false);
+    }
+
+    private void testConnectionReceivesFragmentedData(boolean writeAndFllush) throws Exception {
+
+        final int FRAME_SIZE = 5317;
+
+        ProtonBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(FRAME_SIZE));
+        for (int i = 0; i < FRAME_SIZE; ++i) {
+            sendBuffer.writeByte('A' + (i % 10));
+        }
+
+        try (NettyEchoServer server = createEchoServer()) {
+            server.setMaxFrameSize(FRAME_SIZE);
+            // Server should fragment the data as it goes through
+            server.setFragmentWrites(true);
+            server.start();
+
+            final int port = server.getServerPort();
+
+            List<Transport> transports = new ArrayList<>();
+
+            TransportOptions clientOptions = createTransportOptions();
+            clientOptions.traceBytes(true);
+            clientOptions.webSocketMaxFrameSize(FRAME_SIZE);
+
+            NettyTransportListener wsListener = new NettyTransportListener(true);
+
+            Transport transport = createTransport(clientOptions, createSSLOptions());
+            try {
+                transport.connect(HOSTNAME, port, wsListener).awaitConnect();
+                transports.add(transport);
+                if (writeAndFllush) {
+                    transport.writeAndFlush(ProtonByteBufferAllocator.DEFAULT.allocate());
+                    transport.writeAndFlush(sendBuffer.copy());
+                } else {
+                    transport.write(ProtonByteBufferAllocator.DEFAULT.allocate());
+                    transport.write(sendBuffer.copy());
+                    transport.flush();
+                }
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport instanceof WebSocketTransport);
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    LOG.debug("Checking completion: read {} expecting {}", bytesRead.get(), FRAME_SIZE);
+                    return bytesRead.get() == FRAME_SIZE || !transport.isConnected();
+                }
+            }, 10000, 50));
+
+            assertTrue(transport.isConnected(), "Connection failed while receiving.");
+
+            transport.close();
+
+            assertEquals(2, data.size(), "Expected 2 data packets due to seperate websocket frames");
+
+            ProtonBuffer receivedBuffer = ProtonByteBufferAllocator.DEFAULT.allocate(FRAME_SIZE);
+            for (ProtonBuffer buf : data) {
+               buf.readBytes(receivedBuffer, buf.getReadableBytes());
+            }
+
+            assertEquals(FRAME_SIZE, receivedBuffer.getReadableBytes(), "Unexpected data length");
+            assertEquals(sendBuffer, receivedBuffer, "Unexpected data");
+        } finally {
+            for (ProtonBuffer buf : data) {
+                ((ByteBuf) buf.unwrap()).release();
+            }
+        }
+
+        assertTrue(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testConnectionsSendReceiveLargeDataFailsDueToMaxFrameSize() throws Exception {
+        final int FRAME_SIZE = 1024;
+
+        ProtonBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(FRAME_SIZE));
+        for (int i = 0; i < FRAME_SIZE; ++i) {
+            sendBuffer.writeByte('A');
+        }
+
+        try (NettyEchoServer server = createEchoServer()) {
+            // Server should pass the data through, client should choke on the incoming size.
+            server.setMaxFrameSize(FRAME_SIZE);
+            server.start();
+
+            final int port = server.getServerPort();
+
+            List<Transport> transports = new ArrayList<>();
+
+            final Transport transport = createTransport(createTransportOptions().webSocketMaxFrameSize(FRAME_SIZE / 2), createSSLOptions());
+
+            try {
+                // Transport can't receive anything bigger so it should fail the connection
+                // when data arrives that is larger than this value.
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                transports.add(transport);
+                transport.writeAndFlush(sendBuffer.copy());
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport instanceof WebSocketTransport);
+            assertTrue(Wait.waitFor(() -> !transport.isConnected()), "Transport should have lost connection");
+        }
+
+        assertFalse(exceptions.isEmpty());
+    }
+
+    @Test
+    public void testTransportDetectsConnectionDropWhenServerEnforcesMaxFrameSize() throws Exception {
+        final int FRAME_SIZE = 1024;
+
+        ProtonBuffer sendBuffer = new ProtonNettyByteBuffer(Unpooled.buffer(FRAME_SIZE));
+        for (int i = 0; i < FRAME_SIZE; ++i) {
+            sendBuffer.writeByte('A');
+        }
+
+        try (NettyEchoServer server = createEchoServer()) {
+            // Server won't accept the data as it's to large and will close the connection.
+            server.setMaxFrameSize(FRAME_SIZE / 2);
+            server.start();
+
+            final int port = server.getServerPort();
+
+            List<Transport> transports = new ArrayList<>();
+
+            final Transport transport = createTransport(createTransportOptions().webSocketMaxFrameSize(FRAME_SIZE), createSSLOptions());
+
+            assertTrue(transport instanceof WebSocketTransport);
+
+            try {
+                // Transport allows bigger frames in so that server is the one causing the failure.
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                transports.add(transport);
+                transport.writeAndFlush(sendBuffer.copy());
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(Wait.waitFor(new Wait.Condition() {
+                @Override
+                public boolean isSatisfied() throws Exception {
+                    try {
+                        transport.writeAndFlush(sendBuffer.copy());
+                    } catch (IOException e) {
+                        LOG.info("Transport send caught error:", e);
+                        return true;
+                    }
+
+                    return false;
+                }
+            }, 10000, 10), "Transport should have lost connection");
+
+            transport.close();
+        }
+    }
+
+    @Test
+    public void testConfiguredHttpHeadersArriveAtServer() throws Exception {
+        try (NettyEchoServer server = createEchoServer()) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            TransportOptions clientOptions = createTransportOptions();
+            clientOptions.addWebSocketHeader("test-header1", "FOO");
+            clientOptions.webSocketHeaders().put("test-header2", "BAR");
+
+            final Transport transport = createTransport(clientOptions, createSSLOptions());
+
+            assertTrue(transport instanceof WebSocketTransport);
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                LOG.info("Connected to server:{}:{} as expected.", HOSTNAME, port);
+            } catch (Exception e) {
+                fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
+            }
+
+            assertTrue(transport.isConnected());
+            assertEquals(HOSTNAME, transport.getHost(), "Server host is incorrect");
+            assertEquals(port, transport.getPort(), "Server port is incorrect");
+
+            assertTrue(server.awaitHandshakeCompletion(2000), "HandshakeCompletion not set within given time");
+            HandshakeComplete handshake = server.getHandshakeComplete();
+            assertNotNull(handshake, "completion should not be null");
+            HttpHeaders requestHeaders = handshake.requestHeaders();
+
+            assertTrue(requestHeaders.contains("test-header1"));
+            assertTrue(requestHeaders.contains("test-header2"));
+
+            assertEquals("FOO", requestHeaders.get("test-header1"));
+            assertEquals("BAR", requestHeaders.get("test-header2"));
+
+            transport.close();
+        }
+
+        assertTrue(!transportErrored);  // Normal shutdown does not trigger the event.
+        assertTrue(exceptions.isEmpty());
+        assertTrue(data.isEmpty());
+    }
+
+    private static final String BROKER_JKS_KEYSTORE = "src/test/resources/broker-jks.keystore";
+    private static final String PASSWORD = "password";
+
+    @Test
+    public void testNonSslWebSocketConnectionFailsToSslServer() throws Exception {
+        SslOptions serverSslOptions = new SslOptions();
+        serverSslOptions.keyStoreLocation(BROKER_JKS_KEYSTORE);
+        serverSslOptions.keyStorePassword(PASSWORD);
+        serverSslOptions.verifyHost(false);
+        serverSslOptions.sslEnabled(true);
+
+        try (NettyBlackHoleServer server = new NettyBlackHoleServer(createServerTransportOptions(), serverSslOptions)) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            TransportOptions clientOptions = createTransportOptions();
+
+            final Transport transport = createTransport(clientOptions, createSSLOptions());
+
+            assertTrue(transport instanceof WebSocketTransport);
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                fail("should not have connected");
+            } catch (Exception e) {
+                LOG.trace("Failed to connect with message: {}", e.getMessage());
+            }
+        }
+    }
+
+    @Test
+    public void testWebsocketConnectionToBlackHoleServerTimesOut() throws Exception {
+        try (NettyBlackHoleServer server = new NettyBlackHoleServer(new TransportOptions(), new SslOptions().sslEnabled(false))) {
+            server.start();
+
+            final int port = server.getServerPort();
+
+            TransportOptions clientOptions = createTransportOptions();
+            clientOptions.connectTimeout(25);
+
+            final Transport transport = createTransport(clientOptions, createSSLOptions());
+
+            assertTrue(transport instanceof WebSocketTransport);
+
+            try {
+                transport.connect(HOSTNAME, port, testListener).awaitConnect();
+                fail("should not have connected");
+            } catch (Exception e) {
+                String message = e.getMessage();
+                assertNotNull(message);
+                assertTrue(message.contains("WebSocket handshake timed out"), "Unexpected message: " + message);
+            }
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/X509AliasKeyManagerTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/X509AliasKeyManagerTest.java
new file mode 100644
index 0000000..3964938
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/X509AliasKeyManagerTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+import org.junit.jupiter.api.Test;
+
+public class X509AliasKeyManagerTest {
+
+    @Test
+    public void testNullAliasCausesIAE() {
+        try {
+            new X509AliasKeyManager(null, mock(X509ExtendedKeyManager.class));
+            fail("Expected an exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testChooseClientAliasReturnsGivenAlias() {
+        String wrapperAlias = "wrapperAlias";
+        String myDelegateAlias = "delegateAlias";
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.chooseClientAlias(any(String[].class), any(Principal[].class), any(Socket.class))).thenReturn(myDelegateAlias);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertEquals(wrapperAlias, wrapper.chooseClientAlias(new String[0], new Principal[0], new Socket()), "Expected wrapper alias");
+    }
+
+    @Test
+    public void testChooseServerAliasReturnsGivenAlias() {
+        String wrapperAlias = "wrapperAlias";
+        String myDelegateAlias = "delegateAlias";
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.chooseServerAlias(any(String.class), any(Principal[].class), any(Socket.class))).thenReturn(myDelegateAlias);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertEquals(wrapperAlias, wrapper.chooseServerAlias("", new Principal[0], new Socket()), "Expected wrapper alias");
+    }
+
+    @Test
+    public void testGetCertificateChainDelegates() {
+        String wrapperAlias = "wrapperAlias";
+        X509Certificate[] certs = new X509Certificate[7];
+
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.getCertificateChain(any(String.class))).thenReturn(certs);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertSame(certs, wrapper.getCertificateChain(wrapperAlias), "Different object returned");
+    }
+
+    @Test
+    public void testGetClientAliasesReturnsGivenAliasOnly() {
+        String wrapperAlias = "wrapperAlias";
+        String[] delegateAliases = new String[] { "a", "b", wrapperAlias};
+
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.getClientAliases(any(String.class), any(Principal[].class))).thenReturn(delegateAliases);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertArrayEquals(new String[] { wrapperAlias }, wrapper.getClientAliases("", new Principal[0]), "Expected array containing only the wrapper alias");
+    }
+
+    @Test
+    public void testGetServerAliasesReturnsGivenAliasOnly() {
+        String wrapperAlias = "wrapperAlias";
+        String[] delegateAliases = new String[] { "a", "b", wrapperAlias};
+
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.getServerAliases(any(String.class), any(Principal[].class))).thenReturn(delegateAliases);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertArrayEquals(new String[] { wrapperAlias }, wrapper.getServerAliases("", new Principal[0]), "Expected array containing only the wrapper alias");
+    }
+
+    @Test
+    public void testGetPrivateKeyDelegates() {
+        String wrapperAlias = "wrapperAlias";
+        PrivateKey mockKey = mock(PrivateKey.class);
+
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.getPrivateKey(any(String.class))).thenReturn(mockKey);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertSame(mockKey, wrapper.getPrivateKey(wrapperAlias), "Different object returned");
+    }
+
+    @Test
+    public void testChooseEngineClientAliasReturnsGivenAlias() {
+        String wrapperAlias = "wrapperAlias";
+        String myDelegateAlias = "delegateAlias";
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.chooseEngineClientAlias(any(String[].class), any(Principal[].class), any(SSLEngine.class))).thenReturn(myDelegateAlias);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertEquals(wrapperAlias, wrapper.chooseEngineClientAlias(new String[0], new Principal[0], mock(SSLEngine.class)), "Expected wrapper alias");
+    }
+
+    @Test
+    public void testChooseEngineServerAliasReturnsGivenAlias() {
+        String wrapperAlias = "wrapperAlias";
+        String myDelegateAlias = "delegateAlias";
+        X509ExtendedKeyManager mock = mock(X509ExtendedKeyManager.class);
+        when(mock.chooseEngineServerAlias(any(String.class), any(Principal[].class), any(SSLEngine.class))).thenReturn(myDelegateAlias);
+
+        X509ExtendedKeyManager wrapper = new X509AliasKeyManager(wrapperAlias, mock);
+
+        assertEquals(wrapperAlias, wrapper.chooseEngineServerAlias("", new Principal[0], mock(SSLEngine.class)), "Expected wrapper alias");
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/ExternalMessage.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/ExternalMessage.java
new file mode 100644
index 0000000..61d12b1
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/ExternalMessage.java
@@ -0,0 +1,655 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.client.AdvancedMessage;
+import org.apache.qpid.protonj2.client.Message;
+import org.apache.qpid.protonj2.client.exceptions.ClientException;
+import org.apache.qpid.protonj2.client.impl.ClientMessageSupport;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Section;
+
+public class ExternalMessage<E> implements Message<E> {
+
+    private Header header;
+    private MessageAnnotations messageAnnotations;
+    private Properties properties;
+    private ApplicationProperties applicationProperties;
+    private Footer footer;
+    private Section<E> body;
+
+    private final boolean allowAdvancedConversions;
+
+    public ExternalMessage() {
+        this(false);
+    }
+
+    public ExternalMessage(boolean allowAdvancedConversions) {
+        this(null, allowAdvancedConversions);
+    }
+
+    public ExternalMessage(Section<E> section) {
+        this(section, false);
+    }
+
+    public ExternalMessage(Section<E> body, boolean allowAdvancedConversions) {
+        this.body = body;
+        this.allowAdvancedConversions = allowAdvancedConversions;
+    }
+
+    @Override
+    public AdvancedMessage<E> toAdvancedMessage() {
+        if (allowAdvancedConversions) {
+            return new AdvancedExternalMessage<E>(this);
+        } else {
+            throw new UnsupportedOperationException("Test ExternalMessage doesn't support AdvancedMessage conversions");
+        }
+    }
+
+    //----- Entry point for creating new ClientMessage instances.
+
+    public static <V> Message<V> create(Section<V> body) {
+        return new ExternalMessage<V>(body);
+    }
+
+    //----- Message Header API
+
+    @Override
+    public boolean durable() {
+        return header == null ? Header.DEFAULT_DURABILITY : header.isDurable();
+    }
+
+    @Override
+    public ExternalMessage<E> durable(boolean durable) {
+        lazyCreateHeader().setDurable(durable);
+        return this;
+    }
+
+    @Override
+    public byte priority() {
+        return header == null ? Header.DEFAULT_PRIORITY : header.getPriority();
+    }
+
+    @Override
+    public ExternalMessage<E> priority(byte priority) {
+        lazyCreateHeader().setPriority(priority);
+        return this;
+    }
+
+    @Override
+    public long timeToLive() {
+        return header == null ? Header.DEFAULT_TIME_TO_LIVE : header.getTimeToLive();
+    }
+
+    @Override
+    public ExternalMessage<E> timeToLive(long timeToLive) {
+        lazyCreateHeader().setTimeToLive(timeToLive);
+        return this;
+    }
+
+    @Override
+    public boolean firstAcquirer() {
+        return header == null ? Header.DEFAULT_FIRST_ACQUIRER : header.isFirstAcquirer();
+    }
+
+    @Override
+    public ExternalMessage<E> firstAcquirer(boolean firstAcquirer) {
+        lazyCreateHeader().setFirstAcquirer(firstAcquirer);
+        return this;
+    }
+
+    @Override
+    public long deliveryCount() {
+        return header == null ? Header.DEFAULT_DELIVERY_COUNT : header.getDeliveryCount();
+    }
+
+    @Override
+    public ExternalMessage<E> deliveryCount(long deliveryCount) {
+        lazyCreateHeader().setDeliveryCount(deliveryCount);
+        return this;
+    }
+
+    //----- Message Properties access
+
+    @Override
+    public Object messageId() {
+        return properties != null ? properties.getMessageId() : null;
+    }
+
+    @Override
+    public Message<E> messageId(Object messageId) {
+        lazyCreateProperties().setMessageId(messageId);
+        return this;
+    }
+
+    @Override
+    public byte[] userId() {
+        byte[] copyOfUserId = null;
+        if (properties != null && properties.getUserId() != null) {
+            copyOfUserId = properties.getUserId().arrayCopy();
+        }
+
+        return copyOfUserId;
+    }
+
+    @Override
+    public Message<E> userId(byte[] userId) {
+        lazyCreateProperties().setUserId(new Binary(Arrays.copyOf(userId, userId.length)));
+        return this;
+    }
+
+    @Override
+    public String to() {
+        return properties != null ? properties.getTo() : null;
+    }
+
+    @Override
+    public Message<E> to(String to) {
+        lazyCreateProperties().setTo(to);
+        return this;
+    }
+
+    @Override
+    public String subject() {
+        return properties != null ? properties.getSubject() : null;
+    }
+
+    @Override
+    public Message<E> subject(String subject) {
+        lazyCreateProperties().setSubject(subject);
+        return this;
+    }
+
+    @Override
+    public String replyTo() {
+        return properties != null ? properties.getReplyTo() : null;
+    }
+
+    @Override
+    public Message<E> replyTo(String replyTo) {
+        lazyCreateProperties().setReplyTo(replyTo);
+        return this;
+    }
+
+    @Override
+    public Object correlationId() {
+        return properties != null ? properties.getCorrelationId() : null;
+    }
+
+    @Override
+    public Message<E> correlationId(Object correlationId) {
+        lazyCreateProperties().setCorrelationId(correlationId);
+        return this;
+    }
+
+    @Override
+    public String contentType() {
+        return properties != null ? properties.getContentType() : null;
+    }
+
+    @Override
+    public Message<E> contentType(String contentType) {
+        lazyCreateProperties().setContentType(contentType);
+        return this;
+    }
+
+    @Override
+    public String contentEncoding() {
+        return properties != null ? properties.getContentEncoding() : null;
+    }
+
+    @Override
+    public Message<E> contentEncoding(String contentEncoding) {
+        lazyCreateProperties().setContentEncoding(contentEncoding);
+        return this;
+    }
+
+    @Override
+    public long absoluteExpiryTime() {
+        return properties != null ? properties.getAbsoluteExpiryTime() : null;
+    }
+
+    @Override
+    public Message<E> absoluteExpiryTime(long expiryTime) {
+        lazyCreateProperties().setAbsoluteExpiryTime(expiryTime);
+        return this;
+    }
+
+    @Override
+    public long creationTime() {
+        return properties != null ? properties.getCreationTime() : null;
+    }
+
+    @Override
+    public Message<E> creationTime(long createTime) {
+        lazyCreateProperties().setCreationTime(createTime);
+        return this;
+    }
+
+    @Override
+    public String groupId() {
+        return properties != null ? properties.getGroupId() : null;
+    }
+
+    @Override
+    public Message<E> groupId(String groupId) {
+        lazyCreateProperties().setGroupId(groupId);
+        return this;
+    }
+
+    @Override
+    public int groupSequence() {
+        return properties != null ? (int) properties.getGroupSequence() : null;
+    }
+
+    @Override
+    public Message<E> groupSequence(int groupSequence) {
+        lazyCreateProperties().setGroupSequence(groupSequence);
+        return this;
+    }
+
+    @Override
+    public String replyToGroupId() {
+        return properties != null ? properties.getReplyToGroupId() : null;
+    }
+
+    @Override
+    public Message<E> replyToGroupId(String replyToGroupId) {
+        lazyCreateProperties().setReplyToGroupId(replyToGroupId);
+        return this;
+    }
+
+    //----- Message Annotations Access
+
+    @Override
+    public Object annotation(String key) {
+        Object value = null;
+        if (messageAnnotations != null) {
+            value = messageAnnotations.getValue().get(Symbol.valueOf(key));
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean hasAnnotation(String key) {
+        if (messageAnnotations != null && messageAnnotations.getValue() != null) {
+            return messageAnnotations.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasAnnotations() {
+        return messageAnnotations != null &&
+               messageAnnotations.getValue() != null &&
+               messageAnnotations.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeAnnotation(String key) {
+        if (hasAnnotations()) {
+            return messageAnnotations.getValue().remove(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public Message<E> forEachAnnotation(BiConsumer<String, Object> action) {
+        if (hasAnnotations()) {
+            messageAnnotations.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public ExternalMessage<E> annotation(String key, Object value) {
+        lazyCreateMessageAnnotations().getValue().put(Symbol.valueOf(key),value);
+        return this;
+    }
+
+    //----- Application Properties Access
+
+    @Override
+    public Object property(String key) {
+        Object value = null;
+        if (applicationProperties != null) {
+            value = applicationProperties.getValue().get(key);
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean hasProperty(String key) {
+        if (applicationProperties != null && applicationProperties.getValue() != null) {
+            return applicationProperties.getValue().containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasProperties() {
+        return applicationProperties != null &&
+               applicationProperties.getValue() != null &&
+               applicationProperties.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeProperty(String key) {
+        if (hasProperties()) {
+            return applicationProperties.getValue().remove(key);
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public Message<E> forEachProperty(BiConsumer<String, Object> action) {
+        if (hasProperties()) {
+            applicationProperties.getValue().forEach(action);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ExternalMessage<E> property(String key, Object value) {
+        lazyCreateApplicationProperties().getValue().put(key,value);
+        return this;
+    }
+
+    //----- Footer Access
+
+    @Override
+    public Object footer(String key) {
+        Object value = null;
+        if (footer != null) {
+            value = footer.getValue().get(Symbol.valueOf(key));
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean hasFooter(String key) {
+        if (footer != null && footer.getValue() != null) {
+            return footer.getValue().containsKey(Symbol.valueOf(key));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean hasFooters() {
+        return footer != null &&
+               footer.getValue() != null &&
+               footer.getValue().size() > 0;
+    }
+
+    @Override
+    public Object removeFooter(String key) {
+        if (hasFooters()) {
+            return footer.getValue().remove(Symbol.valueOf(key));
+        } else {
+            return null;
+        }
+     }
+
+    @Override
+    public Message<E> forEachFooter(BiConsumer<String, Object> action) {
+        if (hasFooters()) {
+            footer.getValue().forEach((key, value) -> {
+                action.accept(key.toString(), value);
+            });
+        }
+
+        return this;
+    }
+
+    @Override
+    public ExternalMessage<E> footer(String key, Object value) {
+        lazyCreateFooter().getValue().put(Symbol.valueOf(key),value);
+        return this;
+    }
+
+    //----- Message body access
+
+    @Override
+    public E body() {
+        return body.getValue();
+    }
+
+    @Override
+    public ExternalMessage<E> body(E body) {
+        this.body = ClientMessageSupport.createSectionFromValue(body);
+        return this;
+    }
+
+    public ExternalMessage<E> body(Section<E> body) {
+        this.body = body;
+        return this;
+    }
+
+    //----- Internal API
+
+    private Header lazyCreateHeader() {
+        if (header == null) {
+            header = new Header();
+        }
+
+        return header;
+    }
+
+    private Properties lazyCreateProperties() {
+        if (properties == null) {
+            properties = new Properties();
+        }
+
+        return properties;
+    }
+
+    private ApplicationProperties lazyCreateApplicationProperties() {
+        if (applicationProperties == null) {
+            applicationProperties = new ApplicationProperties(new LinkedHashMap<>());
+        }
+
+        return applicationProperties;
+    }
+
+    private MessageAnnotations lazyCreateMessageAnnotations() {
+        if (messageAnnotations == null) {
+            messageAnnotations = new MessageAnnotations(new LinkedHashMap<>());
+        }
+
+        return messageAnnotations;
+    }
+
+    private Footer lazyCreateFooter() {
+        if (footer == null) {
+            footer = new Footer(new LinkedHashMap<>());
+        }
+
+        return footer;
+    }
+
+    //----- Sealed AdvancedMessage implementation that wraps this type.
+
+    private static class AdvancedExternalMessage<E> extends ExternalMessage<E> implements AdvancedMessage<E> {
+
+        private final ExternalMessage<E> message;
+
+        private final List<Section<?>> bodySections = new ArrayList<>();
+        private int messageFormat;
+
+        /**
+         * Create a wrapper that exposes {@link ExternalMessage} as an {@link AdvancedMessage}
+         *
+         * @param message
+         *      this message to wrap.
+         */
+        public AdvancedExternalMessage(ExternalMessage<E> message) {
+            this.message = message;
+        }
+
+        @Override
+        public Header header() {
+            return message.header;
+        }
+
+        @Override
+        public AdvancedMessage<E> header(Header header) {
+            message.header = header;
+            return this;
+        }
+
+        @Override
+        public MessageAnnotations annotations() {
+            return message.messageAnnotations;
+        }
+
+        @Override
+        public AdvancedMessage<E> annotations(MessageAnnotations messageAnnotations) {
+            message.messageAnnotations = messageAnnotations;
+            return this;
+        }
+
+        @Override
+        public Properties properties() {
+            return message.properties;
+        }
+
+        @Override
+        public AdvancedMessage<E> properties(Properties properties) {
+            message.properties = properties;
+            return this;
+        }
+
+        @Override
+        public ApplicationProperties applicationProperties() {
+            return message.applicationProperties;
+        }
+
+        @Override
+        public AdvancedMessage<E> applicationProperties(ApplicationProperties applicationProperties) {
+            message.applicationProperties = applicationProperties;
+            return this;
+        }
+
+        @Override
+        public Footer footer() {
+            return message.footer;
+        }
+
+        @Override
+        public AdvancedMessage<E> footer(Footer footer) {
+            message.footer = footer;
+            return this;
+        }
+
+        @Override
+        public int messageFormat() {
+            return messageFormat;
+        }
+
+        @Override
+        public AdvancedMessage<E> messageFormat(int messageFormat) {
+            this.messageFormat = messageFormat;
+            return this;
+        }
+
+        @Override
+        public ProtonBuffer encode(Map<String, Object> deliveryAnnotations) throws ClientException {
+            return ClientMessageSupport.encodeMessage(this, deliveryAnnotations);
+        }
+
+        @Override
+        public AdvancedMessage<E> addBodySection(Section<?> bodySection) {
+            Objects.requireNonNull(bodySection, "Additional Body Section cannot be null");
+
+            if (bodySections.isEmpty()) {
+                // Preserve older section from original message creation.
+                if (message.body() != null) {
+                    bodySections.add(message.body);
+                }
+
+                message.body = null;
+            }
+
+            return this;
+        }
+
+        @Override
+        public AdvancedMessage<E> bodySections(Collection<Section<?>> sections) {
+            this.bodySections.clear();
+            this.bodySections.addAll(sections);
+            return this;
+        }
+
+        @Override
+        public Collection<Section<?>> bodySections() {
+            return Collections.unmodifiableCollection(bodySections);
+        }
+
+        @Override
+        public AdvancedMessage<E> forEachBodySection(Consumer<Section<?>> consumer) {
+            if (!bodySections.isEmpty()) {
+                bodySections.forEach(section -> {
+                    consumer.accept(section);
+                });
+            } else {
+                if (message.body != null) {
+                    consumer.accept(message.body);
+                }
+            }
+
+            return this;
+        }
+
+        @Override
+        public AdvancedMessage<E> clearBodySections() {
+            bodySections.clear();
+            message.body = null;
+
+            return this;
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/IOExceptionSupportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/IOExceptionSupportTest.java
new file mode 100644
index 0000000..5c9618f
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/IOExceptionSupportTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+class IOExceptionSupportTest {
+
+    @Test
+    void testCreateFromEmptyException() {
+        IOException ex = IOExceptionSupport.create(new RuntimeException());
+
+        assertNotNull(ex.getMessage());
+        assertNotNull(ex.getCause());
+        assertTrue(ex.getCause() instanceof RuntimeException);
+        assertTrue(ex.getMessage().contains("RuntimeException"));
+    }
+
+    @Test
+    void testCreateFromRuntimeException() {
+        RuntimeException cause = new RuntimeException("Error");
+        IOException ex = IOExceptionSupport.create(cause);
+
+        assertNotNull(ex.getMessage());
+        assertNotNull(ex.getCause());
+        assertTrue(ex.getCause() instanceof RuntimeException);
+        assertSame(ex.getCause(), cause);
+        assertEquals(ex.getMessage(), cause.getMessage());
+    }
+
+    @Test
+    void testCreateFromRuntimeExceptionEmptyStrngInMessage() {
+        RuntimeException cause = new RuntimeException("");
+        IOException ex = IOExceptionSupport.create(cause);
+
+        assertNotNull(ex.getMessage());
+        assertFalse(ex.getMessage().isEmpty());
+        assertNotNull(ex.getCause());
+        assertTrue(ex.getCause() instanceof RuntimeException);
+        assertSame(ex.getCause(), cause);
+        assertNotEquals(ex.getMessage(), cause.getMessage());
+        assertTrue(ex.getMessage().contains("RuntimeException"));
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/IdGeneratorTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/IdGeneratorTest.java
new file mode 100644
index 0000000..9dc4738
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/IdGeneratorTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class IdGeneratorTest {
+
+    IdGenerator generator;
+
+    @BeforeEach
+    public void setUp() {
+        generator = new IdGenerator();
+    }
+
+    @Test
+    public void testDefaultPrefix() {
+        String generated = generator.generateId();
+        assertTrue(generated.startsWith(IdGenerator.DEFAULT_PREFIX));
+        assertFalse(generated.substring(IdGenerator.DEFAULT_PREFIX.length()).startsWith(":"));
+    }
+
+    @Test
+    public void testNonDefaultPrefix() {
+        generator = new IdGenerator("TEST-");
+        String generated = generator.generateId();
+        assertFalse(generated.startsWith(IdGenerator.DEFAULT_PREFIX));
+        assertFalse(generated.substring("TEST-".length()).startsWith(":"));
+    }
+
+    @Test
+    public void testIdIndexIncrements() throws Exception {
+
+        final int COUNT = 5;
+
+        ArrayList<String> ids = new ArrayList<String>(COUNT);
+        ArrayList<Integer> sequences = new ArrayList<Integer>();
+
+        for (int i = 0; i < COUNT; ++i) {
+            ids.add(generator.generateId());
+        }
+
+        for (String id : ids) {
+            String[] components = id.split(":");
+            sequences.add(Integer.parseInt(components[components.length - 1]));
+        }
+
+        Integer lastValue = null;
+        for (Integer sequence : sequences) {
+            if (lastValue != null) {
+                assertTrue(sequence > lastValue);
+            }
+
+            lastValue = sequence;
+        }
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/RecoonectionURIPoolTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/RecoonectionURIPoolTest.java
new file mode 100644
index 0000000..737f0ed
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/RecoonectionURIPoolTest.java
@@ -0,0 +1,484 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class RecoonectionURIPoolTest {
+
+    private List<URI> uris;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        uris = new ArrayList<>();
+
+        uris.add(new URI("tcp://192.168.2.1:5672"));
+        uris.add(new URI("tcp://192.168.2.2:5672"));
+        uris.add(new URI("tcp://192.168.2.3:5672"));
+        uris.add(new URI("tcp://192.168.2.4:5672"));
+    }
+
+    @Test
+    public void testCreateEmptyPool() {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        assertEquals(0, pool.size());
+        assertNotNull(pool.toString());
+    }
+
+    @Test
+    public void testCreateEmptyPoolFromNullUris() {
+        ReconnectionURIPool pool = new ReconnectionURIPool(null);
+        assertNull(pool.getNext());
+    }
+
+    @Test
+    public void testCreateNonEmptyPoolWithURIs() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+
+        assertEquals(uris, pool.getList());
+        assertNotNull(pool.getNext());
+        assertEquals(uris.get(1), pool.getNext());
+    }
+
+    @Test
+    public void testGetNextFromEmptyPool() {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+        assertNull(pool.getNext());
+    }
+
+    @Test
+    public void testGetNextFromSingleValuePool() {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris.subList(0, 1));
+
+        assertEquals(uris.get(0), pool.getNext());
+        assertEquals(uris.get(0), pool.getNext());
+        assertEquals(uris.get(0), pool.getNext());
+
+        assertNotNull(pool.toString());
+    }
+
+    @Test
+    public void testAddUriToEmptyPool() {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+        assertTrue(pool.isEmpty());
+        pool.add(uris.get(0));
+        assertFalse(pool.isEmpty());
+        assertEquals(uris.get(0), pool.getNext());
+    }
+
+    @Test
+    public void testDuplicatesNotAdded() {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+
+        assertEquals(uris.size(), pool.size());
+        pool.add(uris.get(0));
+        assertEquals(uris.size(), pool.size());
+        pool.add(uris.get(1));
+        assertEquals(uris.size(), pool.size());
+    }
+
+    @Test
+    public void testDuplicatesNotAddedByAddFirst() {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+
+        assertEquals(uris.size(), pool.size());
+        pool.addFirst(uris.get(0));
+        assertEquals(uris.size(), pool.size());
+        pool.addFirst(uris.get(1));
+        assertEquals(uris.size(), pool.size());
+    }
+
+    @Test
+    public void testDuplicatesNotAddedWhenQueryPresent() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://127.0.0.1:5672?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://localhost:5672?transport.tcpNoDelay=true"));
+        assertEquals(1, pool.size());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://localhost:5672?transport.tcpNoDelay=false"));
+        assertEquals(1, pool.size());
+    }
+
+    @Test
+    public void testDuplicatesNotAddedWithHostResolution() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://127.0.0.1:5672"));
+        assertFalse(pool.isEmpty());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://localhost:5672"));
+        assertEquals(1, pool.size());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://localhost:5673"));
+        assertEquals(2, pool.size());
+    }
+
+    @Test
+    public void testDuplicatesNotAddedUnresolvable() throws Exception {
+        assumeFalse(checkIfResolutionWorks(), "Host resolution works when not expected");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672"));
+        assertFalse(pool.isEmpty());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672"));
+        assertEquals(1, pool.size());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://SHOULDBEUNRESOLVABLE:5672"));
+        assertEquals(1, pool.size());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://SHOULDBEUNRESOLVABLE2:5672"));
+        assertEquals(2, pool.size());
+    }
+
+    @Test
+    public void testDuplicatesNotAddedWhenQueryPresentAndUnresolveable() throws URISyntaxException {
+        assumeFalse(checkIfResolutionWorks(), "Host resolution works when not expected");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672?transport.tcpNoDelay=false"));
+        assertEquals(1, pool.size());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://SHOULDBEUNRESOLVABLE:5672?transport.tcpNoDelay=true"));
+        assertEquals(1, pool.size());
+
+        assertEquals(1, pool.size());
+        pool.add(new URI("tcp://SHOULDBEUNRESOLVABLE2:5672?transport.tcpNoDelay=true"));
+        assertEquals(2, pool.size());
+    }
+
+    @Test
+    public void testAddUriToPoolThenShuffle() throws URISyntaxException {
+        URI newUri = new URI("tcp://192.168.2." + (uris.size() + 1) + ":5672");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        pool.add(newUri);
+
+        pool.shuffle();
+
+        URI found = null;
+
+        for (int i = 0; i < uris.size() + 1; ++i) {
+            URI next = pool.getNext();
+            if (newUri.equals(next)) {
+                found = next;
+            }
+        }
+
+        if (found == null) {
+            fail("URI added was not retrieved from the pool");
+        }
+    }
+
+    @Test
+    public void testAddUriToPoolNotRandomized() throws URISyntaxException {
+        URI newUri = new URI("tcp://192.168.2." + (uris.size() + 1) + ":5672");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        pool.shuffle();
+        pool.add(newUri);
+
+        for (int i = 0; i < uris.size(); ++i) {
+            assertNotEquals(newUri, pool.getNext());
+        }
+
+        assertEquals(newUri, pool.getNext());
+    }
+
+    @Test
+    public void testAddFirst() throws URISyntaxException {
+        URI newUri = new URI("tcp://192.168.2." + (uris.size() + 1) + ":5672");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        pool.addFirst(newUri);
+
+        assertEquals(newUri, pool.getNext());
+
+        for (int i = 0; i < uris.size(); ++i) {
+            assertNotEquals(newUri, pool.getNext());
+        }
+
+        assertEquals(newUri, pool.getNext());
+    }
+
+    @Test
+    public void testAddFirstHandlesNulls() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        pool.addFirst(null);
+
+        assertEquals(uris.size(), pool.size());
+    }
+
+    @Test
+    public void testAddFirstToEmptyPool() {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+        assertTrue(pool.isEmpty());
+        pool.addFirst(uris.get(0));
+        assertFalse(pool.isEmpty());
+        assertEquals(uris.get(0), pool.getNext());
+    }
+
+    @Test
+    public void testAddAllHandlesNulls() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        pool.addAll(null);
+
+        assertEquals(uris.size(), pool.size());
+    }
+
+    @Test
+    public void testAddAllHandlesEmpty() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        pool.addAll(Collections.emptyList());
+
+        assertEquals(uris.size(), pool.size());
+    }
+
+    @Test
+    public void testAddAll() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(null);
+
+        assertEquals(0, pool.size());
+        assertFalse(uris.isEmpty());
+
+        pool.addAll(uris);
+
+        assertEquals(uris.size(), pool.size());
+    }
+
+    @Test
+    public void testRemoveURIFromPool() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+
+        URI removed = uris.get(0);
+
+        pool.remove(removed);
+
+        for (int i = 0; i < uris.size() + 1; ++i) {
+            if (removed.equals(pool.getNext())) {
+                fail("URI was not removed from the pool");
+            }
+        }
+    }
+
+    @Test
+    public void testRemovedWhenQueryPresent() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://127.0.0.1:5672?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://localhost:5672?transport.tcpNoDelay=true"));
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://127.0.0.1:5672?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://localhost:5672?transport.tcpNoDelay=false"));
+        assertTrue(pool.isEmpty());
+    }
+
+    @Test
+    public void testRemoveWithHostResolution() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://127.0.0.1:5672"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://localhost:5672"));
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://127.0.0.1:5672"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://localhost:5673"));
+        assertFalse(pool.isEmpty());
+    }
+
+    @Test
+    public void testRemoveWhenUnresolvable() throws URISyntaxException {
+        assumeFalse(checkIfResolutionWorks(), "Host resolution works when not expected");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://SHOULDBEUNRESOLVABLE:5672"));
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://shouldbeunresolvable:5673"));
+        assertFalse(pool.isEmpty());
+    }
+
+    @Test
+    public void testRemoveWhenQueryPresentAndUnresolveable() throws URISyntaxException {
+        assumeFalse(checkIfResolutionWorks(), "Host resolution works when not expected");
+
+        ReconnectionURIPool pool = new ReconnectionURIPool();
+
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://SHOULDBEUNRESOLVABLE:5672?transport.tcpNoDelay=true"));
+        assertTrue(pool.isEmpty());
+        pool.add(new URI("tcp://shouldbeunresolvable:5672?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+        pool.remove(new URI("tcp://shouldbeunresolvable:5673?transport.tcpNoDelay=true"));
+        assertFalse(pool.isEmpty());
+    }
+
+    @Test
+    public void testConnectedShufflesWhenRandomizing() {
+        assertConnectedEffectOnPool(true, true);
+    }
+
+    @Test
+    public void testConnectedDoesNotShufflesWhenNoRandomizing() {
+        assertConnectedEffectOnPool(false, false);
+    }
+
+    private void assertConnectedEffectOnPool(boolean randomize, boolean shouldShuffle) {
+
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+
+        if (randomize) {
+            pool.shuffle();
+        }
+
+        List<URI> current = new ArrayList<>();
+        List<URI> previous = new ArrayList<>();
+
+        boolean shuffled = false;
+
+        for (int i = 0; i < 10; ++i) {
+
+            for (int j = 0; j < uris.size(); ++j) {
+                current.add(pool.getNext());
+            }
+
+            if (randomize) {
+                pool.shuffle();
+            }
+
+            if (!previous.isEmpty() && !previous.equals(current)) {
+                shuffled = true;
+                break;
+            }
+
+            previous.clear();
+            previous.addAll(current);
+            current.clear();
+        }
+
+        if (shouldShuffle) {
+            assertTrue(shuffled, "URIs did not get randomized");
+        } else {
+            assertFalse(shuffled, "URIs should not get randomized");
+        }
+    }
+
+    @Test
+    public void testAddOrRemoveNullHasNoAffect() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        assertEquals(uris.size(), pool.size());
+
+        pool.add(null);
+        assertEquals(uris.size(), pool.size());
+        pool.remove(null);
+        assertEquals(uris.size(), pool.size());
+    }
+
+    private boolean checkIfResolutionWorks() {
+        boolean resolutionWorks = false;
+        try {
+            resolutionWorks = InetAddress.getByName("shouldbeunresolvable") != null;
+            resolutionWorks = InetAddress.getByName("SHOULDBEUNRESOLVABLE") != null;
+            resolutionWorks = InetAddress.getByName("SHOULDBEUNRESOLVABLE2") != null;
+        } catch (Exception e) {
+        }
+
+        return resolutionWorks;
+    }
+
+    @Test
+    public void testRemoveAll() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        assertEquals(uris.size(), pool.size());
+
+        pool.removeAll();
+        assertTrue(pool.isEmpty());
+        assertEquals(0, pool.size());
+
+        pool.removeAll();
+    }
+
+    @Test
+    public void testReplaceAll() throws URISyntaxException {
+        ReconnectionURIPool pool = new ReconnectionURIPool(uris);
+        assertEquals(uris.size(), pool.size());
+
+        List<URI> newUris = new ArrayList<>();
+
+        newUris.add(new URI("tcp://192.168.2.1:5672"));
+        newUris.add(new URI("tcp://192.168.2.2:5672"));
+
+        pool.replaceAll(newUris);
+        assertFalse(pool.isEmpty());
+        assertEquals(newUris.size(), pool.size());
+        assertEquals(newUris, pool.getList());
+
+        pool.removeAll();
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/StopWatchTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/StopWatchTest.java
new file mode 100644
index 0000000..03328e7
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/StopWatchTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class StopWatchTest {
+
+    @Test
+    void testCreateDefaultStarts() throws Exception {
+        StopWatch watch = new StopWatch();
+        assertTrue(watch.isStarted());
+        Thread.sleep(1);
+        assertTrue(watch.stop() > 0);
+    }
+
+    @Test
+    void testCreateStarted() throws Exception {
+        StopWatch watch = new StopWatch(true);
+        assertTrue(watch.isStarted());
+        Thread.sleep(1);
+        assertTrue(watch.stop() > 0);
+    }
+
+    @Test
+    void testCreateNotStarted() throws Exception {
+        StopWatch watch = new StopWatch(false);
+        assertFalse(watch.isStarted());
+        Thread.sleep(1);
+        assertEquals(0, watch.stop());
+    }
+
+    @Test
+    void testRestart() throws Exception {
+        StopWatch watch = new StopWatch();
+        assertTrue(watch.isStarted());
+        Thread.sleep(1);
+        long taken1 = watch.taken();
+        Thread.sleep(2);
+        long taken2 = watch.taken();
+        assertNotEquals(taken1, taken2);
+        long taken3 = watch.stop();
+        assertEquals(taken3, watch.taken());
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/StringArrayConverterTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/StringArrayConverterTest.java
new file mode 100644
index 0000000..7ed7a45
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/StringArrayConverterTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.junit.jupiter.api.Test;
+
+public class StringArrayConverterTest extends ImperativeClientTestCase {
+
+    private class NulToStringValue {
+
+        @Override
+        public String toString() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testCreate() {
+        new StringArrayConverter();
+    }
+
+    @Test
+    public void testConvertToStringArray() throws Exception {
+        assertNull(StringArrayConverter.convertToStringArray(null));
+        assertNull(StringArrayConverter.convertToStringArray(""));
+        assertNull(StringArrayConverter.convertToStringArray(new NulToStringValue()));
+
+        String[] array = StringArrayConverter.convertToStringArray("foo");
+        assertEquals(1, array.length);
+        assertEquals("foo", array[0]);
+
+        array = StringArrayConverter.convertToStringArray("foo,bar");
+        assertEquals(2, array.length);
+        assertEquals("foo", array[0]);
+        assertEquals("bar", array[1]);
+
+        array = StringArrayConverter.convertToStringArray("foo,bar,baz");
+        assertEquals(3, array.length);
+        assertEquals("foo", array[0]);
+        assertEquals("bar", array[1]);
+        assertEquals("baz", array[2]);
+    }
+
+    @Test
+    public void testConvertToString() throws Exception {
+        assertNull(StringArrayConverter.convertToString(null));
+        assertNull(StringArrayConverter.convertToString(new String[]{}));
+
+        assertEquals("", StringArrayConverter.convertToString(new String[]{""}));
+        assertEquals("foo", StringArrayConverter.convertToString(new String[]{"foo"}));
+        assertEquals("foo,bar", StringArrayConverter.convertToString(new String[]{"foo", "bar"}));
+        assertEquals("foo,bar,baz", StringArrayConverter.convertToString(new String[]{"foo", "bar", "baz"}));
+    }
+}
diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/ThreadPoolUtilsTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/ThreadPoolUtilsTest.java
new file mode 100644
index 0000000..be80488
--- /dev/null
+++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/util/ThreadPoolUtilsTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.client.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.client.test.ImperativeClientTestCase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test for ThreadPoolUtis support class.
+ */
+@Timeout(10)
+public class ThreadPoolUtilsTest extends ImperativeClientTestCase {
+
+    @Test
+    public void testCreate() {
+        new ThreadPoolUtils();
+    }
+
+    @Test
+    public void testShutdown() throws Exception {
+        ExecutorService service = Executors.newSingleThreadExecutor();
+        ThreadPoolUtils.shutdown(service);
+        assertTrue(service.isShutdown());
+    }
+
+    @Test
+    public void testShutdownNullService() throws Exception {
+        ThreadPoolUtils.shutdown(null);
+    }
+
+    @Test
+    public void testShutdownNowWithNoTasks() throws Exception {
+        ExecutorService service = Executors.newSingleThreadExecutor();
+        assertNotNull(ThreadPoolUtils.shutdownNow(service));
+        assertTrue(service.isShutdown());
+    }
+
+    @Test
+    public void testShutdownNowReturnsUnexecuted() throws Exception {
+        final CountDownLatch started = new CountDownLatch(1);
+        final CountDownLatch finish = new CountDownLatch(1);
+        ExecutorService service = Executors.newSingleThreadExecutor();
+
+        service.execute(new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    started.countDown();
+                    finish.await(10, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                }
+            }
+        });
+
+        service.execute(new Runnable() {
+
+            @Override
+            public void run() {
+            }
+        });
+        service.execute(new Runnable() {
+
+            @Override
+            public void run() {
+            }
+        });
+
+        assertTrue(started.await(5, TimeUnit.SECONDS));
+
+        List<Runnable> notRun = ThreadPoolUtils.shutdownNow(service);
+        assertTrue(service.isShutdown());
+        finish.countDown();
+        assertTrue(ThreadPoolUtils.awaitTermination(service, 1000));
+
+        assertEquals(2, notRun.size());
+    }
+
+    @Test
+    public void testShutdownNowAlreadyShutdown() throws Exception {
+        ExecutorService service = Executors.newSingleThreadExecutor();
+        service.shutdown();
+        assertNotNull(ThreadPoolUtils.shutdownNow(service));
+        assertTrue(service.isShutdown());
+    }
+
+    @Test
+    public void testShutdownNowNullService() throws Exception {
+        assertNotNull(ThreadPoolUtils.shutdownNow(null));
+    }
+
+    @Test
+    public void testShutdownGraceful() throws Exception {
+        ExecutorService service = Executors.newSingleThreadExecutor();
+        ThreadPoolUtils.shutdownGraceful(service);
+        assertTrue(service.isShutdown());
+    }
+
+    @Test
+    public void testShutdownGracefulWithTimeout() throws Exception {
+        ExecutorService service = Executors.newSingleThreadExecutor();
+        ThreadPoolUtils.shutdownGraceful(service, 1000);
+        assertTrue(service.isShutdown());
+    }
+
+    @Test
+    public void testShutdownGracefulWithStuckTask() throws Exception {
+        final CountDownLatch started = new CountDownLatch(1);
+        final CountDownLatch finish = new CountDownLatch(1);
+        ExecutorService service = Executors.newSingleThreadExecutor();
+
+        service.execute(new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    started.countDown();
+                    finish.await(10, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                }
+            }
+        });
+
+        assertTrue(started.await(5, TimeUnit.SECONDS));
+
+        ThreadPoolUtils.shutdownGraceful(service, 100);
+        assertTrue(service.isShutdown());
+        finish.countDown();
+        assertTrue(ThreadPoolUtils.awaitTermination(service, 1000));
+    }
+
+    @Test
+    public void testAwaitTerminationWithNullService() throws Exception {
+        assertTrue(ThreadPoolUtils.awaitTermination(null, 1000));
+    }
+}
diff --git a/protonj2-client/src/test/resources/README.txt b/protonj2-client/src/test/resources/README.txt
new file mode 100644
index 0000000..8e7b215
--- /dev/null
+++ b/protonj2-client/src/test/resources/README.txt
@@ -0,0 +1,88 @@
+# The various SSL stores and certificates were created with the following commands:
+# Requires use of JDK 8+ keytool command.
+
+
+# Clean up existing files
+# -----------------------
+rm -f *.crt *.csr *.keystore *.truststore
+
+# Create a key and self-signed certificate for the CA, to sign certificate requests and use for trust:
+# ----------------------------------------------------------------------------------------------------
+keytool -storetype pkcs12 -keystore ca-pkcs12.keystore -storepass password -keypass password -alias ca -genkey -keyalg "RSA" -keysize 2048 -dname "O=My Trusted Inc.,CN=my-ca.org" -validity 9999 -ext bc:c=ca:true
+keytool -storetype pkcs12 -keystore ca-pkcs12.keystore -storepass password -alias ca -exportcert -rfc > ca.crt
+
+# Create a key pair for the broker, and sign it with the CA:
+# ----------------------------------------------------------
+keytool -storetype pkcs12 -keystore broker-pkcs12.keystore -storepass password -keypass password -alias broker -genkey -keyalg "RSA" -keysize 2048 -dname "O=Server,CN=localhost" -validity 9999 -ext bc=ca:false -ext eku=sA
+
+keytool -storetype pkcs12 -keystore broker-pkcs12.keystore -storepass password -alias broker -certreq -file broker.csr
+keytool -storetype pkcs12 -keystore ca-pkcs12.keystore -storepass password -alias ca -gencert -rfc -infile broker.csr -outfile broker.crt -validity 9999 -ext bc=ca:false -ext eku=sA
+
+keytool -storetype pkcs12 -keystore broker-pkcs12.keystore -storepass password -keypass password -importcert -alias ca -file ca.crt -noprompt
+keytool -storetype pkcs12 -keystore broker-pkcs12.keystore -storepass password -keypass password -importcert -alias broker -file broker.crt
+
+# Create some alternative keystore types for testing:
+# ---------------------------------------------------
+keytool -importkeystore -srckeystore broker-pkcs12.keystore -destkeystore broker-jceks.keystore -srcstoretype pkcs12 -deststoretype jceks -srcstorepass password -deststorepass password
+keytool -importkeystore -srckeystore broker-pkcs12.keystore -destkeystore broker-jks.keystore -srcstoretype pkcs12 -deststoretype jks -srcstorepass password -deststorepass password
+
+# Create a key pair for the broker with an unexpected hostname, and sign it with the CA:
+# --------------------------------------------------------------------------------------
+keytool -storetype jks -keystore broker-wrong-host-jks.keystore -storepass password -keypass password -alias broker-wrong-host -genkey -keyalg "RSA" -keysize 2048 -dname "O=Server,CN=wronghost" -validity 9999 -ext bc=ca:false -ext eku=sA
+
+keytool -storetype jks -keystore broker-wrong-host-jks.keystore -storepass password -alias broker-wrong-host -certreq -file broker-wrong-host.csr
+keytool -storetype pkcs12 -keystore ca-pkcs12.keystore -storepass password -alias ca -gencert -rfc -infile broker-wrong-host.csr -outfile broker-wrong-host.crt -validity 9999 -ext bc=ca:false -ext eku=sA
+
+keytool -storetype jks -keystore broker-wrong-host-jks.keystore -storepass password -keypass password -importcert -alias ca -file ca.crt -noprompt
+keytool -storetype jks -keystore broker-wrong-host-jks.keystore -storepass password -keypass password -importcert -alias broker-wrong-host -file broker-wrong-host.crt
+
+# Create trust stores for the broker, import the CA cert:
+# -------------------------------------------------------
+keytool -storetype pkcs12 -keystore broker-pkcs12.truststore -storepass password -keypass password -importcert -alias ca -file ca.crt -noprompt
+keytool -importkeystore -srckeystore broker-pkcs12.truststore -destkeystore broker-jceks.truststore -srcstoretype pkcs12 -deststoretype jceks -srcstorepass password -deststorepass password
+keytool -importkeystore -srckeystore broker-pkcs12.truststore -destkeystore broker-jks.truststore -srcstoretype pkcs12 -deststoretype jks -srcstorepass password -deststorepass password
+
+# Create a key pair for the client, and sign it with the CA:
+# ----------------------------------------------------------
+keytool -storetype pkcs12 -keystore client-pkcs12.keystore -storepass password -keypass password -alias client -genkey -keyalg "RSA" -keysize 2048 -dname "O=Client,CN=client" -validity 9999 -ext bc=ca:false -ext eku=cA
+
+keytool -storetype pkcs12 -keystore client-pkcs12.keystore -storepass password -alias client -certreq -file client.csr
+keytool -storetype pkcs12 -keystore ca-pkcs12.keystore -storepass password -alias ca -gencert -rfc -infile client.csr -outfile client.crt -validity 9999 -ext bc=ca:false -ext eku=cA
+
+keytool -storetype pkcs12 -keystore client-pkcs12.keystore -storepass password -keypass password -importcert -alias ca -file ca.crt -noprompt
+keytool -storetype pkcs12 -keystore client-pkcs12.keystore -storepass password -keypass password -importcert -alias client -file client.crt
+
+# Create some alternative keystore types for testing:
+# ---------------------------------------------------
+keytool -importkeystore -srckeystore client-pkcs12.keystore -destkeystore client-jceks.keystore -srcstoretype pkcs12 -deststoretype jceks -srcstorepass password -deststorepass password
+keytool -importkeystore -srckeystore client-pkcs12.keystore -destkeystore client-jks.keystore -srcstoretype pkcs12 -deststoretype jks -srcstorepass password -deststorepass password
+
+# Create a key pair for a second client, and sign it with the CA:
+# ----------------------------------------------------------
+keytool -storetype jks -keystore client2-jks.keystore -storepass password -keypass password -alias client2 -genkey -keyalg "RSA" -keysize 2048 -dname "O=Client2,CN=client2" -validity 9999 -ext bc=ca:false -ext eku=cA
+
+keytool -storetype jks -keystore client2-jks.keystore -storepass password -alias client2 -certreq -file client2.csr
+keytool -storetype pkcs12 -keystore ca-pkcs12.keystore -storepass password -alias ca -gencert -rfc -infile client2.csr -outfile client2.crt -validity 9999 -ext bc=ca:false -ext eku=cA
+
+keytool -storetype jks -keystore client2-jks.keystore -storepass password -keypass password -importcert -alias ca -file ca.crt -noprompt
+keytool -storetype jks -keystore client2-jks.keystore -storepass password -keypass password -importcert -alias client2 -file client2.crt
+
+# Create trust stores for the client, import the CA cert:
+# -------------------------------------------------------
+keytool -storetype pkcs12 -keystore client-pkcs12.truststore -storepass password -keypass password -importcert -alias ca -file ca.crt -noprompt
+keytool -importkeystore -srckeystore client-pkcs12.truststore -destkeystore client-jceks.truststore -srcstoretype pkcs12 -deststoretype jceks -srcstorepass password -deststorepass password
+keytool -importkeystore -srckeystore client-pkcs12.truststore -destkeystore client-jks.truststore -srcstoretype pkcs12 -deststoretype jks -srcstorepass password -deststorepass password
+
+# Create a truststore with self-signed certificate for an alternative CA, to
+# allow 'failure to trust' of certs signed by the original CA above:
+# ------------------------------------------------------------------
+keytool -storetype jks -keystore other-ca-jks.truststore -storepass password -keypass password -alias other-ca -genkey -keyalg "RSA" -keysize 2048 -dname "O=Other Trusted Inc.,CN=other-ca.org" -validity 9999 -ext bc:c=ca:true
+keytool -storetype jks -keystore other-ca-jks.truststore -storepass password -alias other-ca -exportcert -rfc > other-ca.crt
+keytool -storetype jks -keystore other-ca-jks.truststore -storepass password -alias other-ca -delete
+keytool -storetype jks -keystore other-ca-jks.truststore -storepass password -keypass password -importcert -alias other-ca -file other-ca.crt -noprompt
+
+# Create a store with multiple key pairs for the client to allow for alias selection:
+# ----------------------------------------------------------
+keytool -importkeystore -srckeystore client-pkcs12.keystore -destkeystore client-multiple-keys-jks.keystore -srcstoretype pkcs12 -deststoretype jks -srcstorepass password -deststorepass password
+keytool -storetype jks -keystore client-multiple-keys-jks.keystore -storepass password -alias ca -delete
+keytool -importkeystore -srckeystore client2-jks.keystore -destkeystore client-multiple-keys-jks.keystore -srcstoretype jks -deststoretype jks -srcstorepass password -deststorepass password
diff --git a/protonj2-client/src/test/resources/SaslGssApiIntegrationTest-login.config b/protonj2-client/src/test/resources/SaslGssApiIntegrationTest-login.config
new file mode 100644
index 0000000..edd7fc1
--- /dev/null
+++ b/protonj2-client/src/test/resources/SaslGssApiIntegrationTest-login.config
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+KRB5-CLIENT {
+    com.sun.security.auth.module.Krb5LoginModule required
+    principal="clientprincipal"
+    useKeyTab=true
+    keytab="target/SaslGssApiIntegrationTest.krb5.keytab";
+};
+
+KRB5-CLIENT-URI-USERNAME-CALLBACK {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    keytab="target/SaslGssApiIntegrationTest.krb5.keytab";
+};
+
+KRB5-CLIENT-FACTORY-USERNAME-CALLBACK {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    keytab="target/SaslGssApiIntegrationTest.krb5.keytab";
+};
+
+amqp-jms-client {
+    com.sun.security.auth.module.Krb5LoginModule required
+    principal="defaultscopeprincipal"
+    useKeyTab=true
+    keytab="target/SaslGssApiIntegrationTest.krb5.keytab";
+};
diff --git a/protonj2-client/src/test/resources/broker-jceks.keystore b/protonj2-client/src/test/resources/broker-jceks.keystore
new file mode 100644
index 0000000..b5a155e
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-jceks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-jceks.truststore b/protonj2-client/src/test/resources/broker-jceks.truststore
new file mode 100644
index 0000000..54a9fd5
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-jceks.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-jks.keystore b/protonj2-client/src/test/resources/broker-jks.keystore
new file mode 100644
index 0000000..a6644f0
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-jks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-jks.truststore b/protonj2-client/src/test/resources/broker-jks.truststore
new file mode 100644
index 0000000..60031b6
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-jks.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-pkcs12.keystore b/protonj2-client/src/test/resources/broker-pkcs12.keystore
new file mode 100644
index 0000000..89f11c3
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-pkcs12.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-pkcs12.truststore b/protonj2-client/src/test/resources/broker-pkcs12.truststore
new file mode 100644
index 0000000..9b6d32a
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-pkcs12.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-wrong-host-jks.keystore b/protonj2-client/src/test/resources/broker-wrong-host-jks.keystore
new file mode 100644
index 0000000..9bb6b06
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-wrong-host-jks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/broker-wrong-host.crt b/protonj2-client/src/test/resources/broker-wrong-host.crt
new file mode 100644
index 0000000..9df826a
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-wrong-host.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDMzCCAhugAwIBAgIEG6o9qDANBgkqhkiG9w0BAQsFADAuMRIwEAYDVQQDEwlt

+eS1jYS5vcmcxGDAWBgNVBAoTD015IFRydXN0ZWQgSW5jLjAeFw0xODA1MTExODU5

+NDVaFw00NTA5MjUxODU5NDVaMCUxEjAQBgNVBAMTCXdyb25naG9zdDEPMA0GA1UE

+ChMGU2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0jeU6oMW

+gK12M01JJjBy4edr6uU43EZDE2bNQbvRyvNtTug97d3V6vuxoYd3MXX98vd5VF6+

+IOlm8YvWBJU0HHmPS9w+mW8LHajZ3/lGVs88Yv2QezWCAch+wh/aOtqK8W4A62VY

+Z/4ju1Srv2VI+lh65+D4b3zjPqSTpFMtWD9mm3923YarmbJhTABEOEjFrKB62cif

+KZHN3I9C9nDSIcy7EVzN7JIzGoBPL2xKGRDemKy999nziTR/6Q6N6m+wHDm4ct9Q

+Cp6e3cSmsOfUs6UmXu5cAVfDwVHdN83cL6aoW4PYEIqDy301a8/+c+JZoSOnPAOe

+Xz1Ub3e5L42swQIDAQABo2IwYDAfBgNVHSMEGDAWgBS6YoKEP39UxVgaoMYECfk7

++zcevTAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBQ2

+xopwads0L3dn5OsDOpzl3t5EEjANBgkqhkiG9w0BAQsFAAOCAQEAJA7ds4Sj/pY7

+2OlQzPgMt1R/CGV9rcmunMVkcJhz1SsvkVGhIa84iL+gYf+TOPTjTU0+M8NhPkD/

+vgD1Qf3s8luijbII+gK481hEvOhYVjTE+pqZsPJNpcjDTt+Sv29Ud5fQ6gA6qXdE

+PoqhnKjZebQHtJZe2lbTUiPHTkEziRbXrwySZTJtx89Emq1xOSqimkW7Fd1TYxe/

+kvWRX9UF3GituPHnHyaTuSA2cXAR2fkfuy9v7S5uo1E5x6vS/IlMuWdVJty5dHnQ

+bTyM8jMs13TlKric9JlmV9DiSm/kII8rfLQ95P9CF9hkar6b6iDJzkvehSh6LFCL

+6HbHNb8vfA==
+-----END CERTIFICATE-----
diff --git a/protonj2-client/src/test/resources/broker-wrong-host.csr b/protonj2-client/src/test/resources/broker-wrong-host.csr
new file mode 100644
index 0000000..96eb0cd
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker-wrong-host.csr
@@ -0,0 +1,16 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIICmjCCAYICAQAwJTESMBAGA1UEAxMJd3Jvbmdob3N0MQ8wDQYDVQQKEwZTZXJ2

+ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSN5TqgxaArXYzTUkm

+MHLh52vq5TjcRkMTZs1Bu9HK821O6D3t3dXq+7Ghh3cxdf3y93lUXr4g6Wbxi9YE

+lTQceY9L3D6ZbwsdqNnf+UZWzzxi/ZB7NYIByH7CH9o62orxbgDrZVhn/iO7VKu/

+ZUj6WHrn4PhvfOM+pJOkUy1YP2abf3bdhquZsmFMAEQ4SMWsoHrZyJ8pkc3cj0L2

+cNIhzLsRXM3skjMagE8vbEoZEN6YrL332fOJNH/pDo3qb7AcObhy31AKnp7dxKaw

+59SzpSZe7lwBV8PBUd03zdwvpqhbg9gQioPLfTVrz/5z4lmhI6c8A55fPVRvd7kv

+jazBAgMBAAGgMDAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBQ2xopwads0L3dn

+5OsDOpzl3t5EEjANBgkqhkiG9w0BAQsFAAOCAQEAWSNkXYOUXhpsvlYi9tu7CPpZ

+v97oI7xljuYIQCdSlkGy9pfwzntt/CXkSJSAzvIs7RnbEvJOC2qW9fq49eBVPcSO

+Fqn3BtPoe8iN2lSLsg76ix7ey81Z0Nqw45DV/Fnmu8RX+PuZvT2kStf9Fbwa/g+L

+ob3IKPPx/lHb54QOm+Bui2Xs3Xx7pEIn3mtKvsth0avC2hRQqR2WBSs2rmzEsb+6

+sX/QxEp4MzFrUAjgJzS3raifR0GBMymsRIBB9xMfJ9quTIkRV8rwfADZzIGzxCWC

+KURuBqvbJNoJrdG7Z5MTQ4l5isXFXYGdKNLy18Wo8Mg4Ds5sQeGfaITeAACSqA==
+-----END NEW CERTIFICATE REQUEST-----
diff --git a/protonj2-client/src/test/resources/broker.crt b/protonj2-client/src/test/resources/broker.crt
new file mode 100644
index 0000000..8d3779e
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDMzCCAhugAwIBAgIEPFLHKzANBgkqhkiG9w0BAQsFADAuMRIwEAYDVQQDEwlt

+eS1jYS5vcmcxGDAWBgNVBAoTD015IFRydXN0ZWQgSW5jLjAeFw0xODA1MTExODU5

+NDNaFw00NTA5MjUxODU5NDNaMCUxEjAQBgNVBAMTCWxvY2FsaG9zdDEPMA0GA1UE

+ChMGU2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi9NcODpQ

+9uVLdDAZASk4VpqjSBhP986IoJrAMj2qTGrTRPaenTaGJD0ZKlucuEc/X9fzXcwt

+eNVvPL3UWPODSnjs+DeilPwoKltjrCw1F82Pv/i2mEjXW16JJxC1S+iYfOcYWIaB

+hkuBxgnrrAaHkYGeQacqUn1l384MIUPwdObSUfBJhCUmtkBqIKj+WI+JB7hl+jWF

+irOi1bjgnNO2TAf1oYQiJdQz05WUqxpVh2t0pHNWzP2fNSkXlLKKc22244jjEoSV

+zREssbjhrOZBr7duV6bxoTCIbBvGpq9z4u8vkJAU+QRIR+NCaJVdvj36yqFiazob

+D/NYDkMwCEsc6QIDAQABo2IwYDAfBgNVHSMEGDAWgBS6YoKEP39UxVgaoMYECfk7

++zcevTAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBQE

+qoXfsB0ugdBiOHuy/ICH/X67gjANBgkqhkiG9w0BAQsFAAOCAQEAXL7k0dCkaDoE

+HMtBnVJR0+kazML5zCRGF/ESXcqyewRbq+0VBjEVFoZMx25ihGMv4Gpudu8HX8RT

+2qmctsa/jUb8UbQWHg/oD8RIn07L6/nas5M7osuSQdSqbNAdniqYpd7HN8E4zWvA

+DQ13x0cOfs3wmPXoSjAHjauApGz9hJW9wNwpBlfTfYqCfwzZ9e77ehmRHo9iQaTJ

+IxAGbRe2WR71wC3/WPyUAtqcY8f76bj7O+4EmjT00Sbxepz5fyEncsLkAUQZjmf0

+NH4VBp2OAa0daFswqMAxYt8dQDyCGjjXBsYbUmqPuiBspN7uFj6/R3+B/Ii/21QA

+r57Z0yciDA==
+-----END CERTIFICATE-----
diff --git a/protonj2-client/src/test/resources/broker.csr b/protonj2-client/src/test/resources/broker.csr
new file mode 100644
index 0000000..8eb495f
--- /dev/null
+++ b/protonj2-client/src/test/resources/broker.csr
@@ -0,0 +1,16 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIICmjCCAYICAQAwJTESMBAGA1UEAxMJbG9jYWxob3N0MQ8wDQYDVQQKEwZTZXJ2

+ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCL01w4OlD25Ut0MBkB

+KThWmqNIGE/3zoigmsAyPapMatNE9p6dNoYkPRkqW5y4Rz9f1/NdzC141W88vdRY

+84NKeOz4N6KU/CgqW2OsLDUXzY+/+LaYSNdbXoknELVL6Jh85xhYhoGGS4HGCeus

+BoeRgZ5BpypSfWXfzgwhQ/B05tJR8EmEJSa2QGogqP5Yj4kHuGX6NYWKs6LVuOCc

+07ZMB/WhhCIl1DPTlZSrGlWHa3Skc1bM/Z81KReUsopzbbbjiOMShJXNESyxuOGs

+5kGvt25XpvGhMIhsG8amr3Pi7y+QkBT5BEhH40JolV2+PfrKoWJrOhsP81gOQzAI

+SxzpAgMBAAGgMDAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBQEqoXfsB0ugdBi

+OHuy/ICH/X67gjANBgkqhkiG9w0BAQsFAAOCAQEATJtvyy+W4XEXPm9IFlDxEtQa

+KO8ByS//Cw3grzeUlZkCQ0vVLeYgogTSbFL4gz1J4U/VICHPLBf7RI9z9PEisLm2

+fBV0XlvPb78NrNj5+BmqBPBWNxDuJFOKtmWqgs9zWkELlkegxD3ywsIt6/CWHzff

+CzZeUcySaViA5AjLupcWpYUa1JQxD//uoJRxAFhVECjVzMvd/un8td3x7K0gbjHl

+GJlFOpg6DJAj88hwTQKz0y84ZqxqqRFF9h6pD6o/V1y+IAjuGRZDmwwpNAUD8JrV

+jpxbzIVZ0tipX6/DPwMP+Rk8KHOUry4gOzzqk3X7TkjTUmrp9FEA/9NQrZiIaA==
+-----END NEW CERTIFICATE REQUEST-----
diff --git a/protonj2-client/src/test/resources/ca-pkcs12.keystore b/protonj2-client/src/test/resources/ca-pkcs12.keystore
new file mode 100644
index 0000000..aece753
--- /dev/null
+++ b/protonj2-client/src/test/resources/ca-pkcs12.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/ca.crt b/protonj2-client/src/test/resources/ca.crt
new file mode 100644
index 0000000..befbe54
--- /dev/null
+++ b/protonj2-client/src/test/resources/ca.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDDDCCAfSgAwIBAgIELTaUXTANBgkqhkiG9w0BAQsFADAuMRIwEAYDVQQDEwlt

+eS1jYS5vcmcxGDAWBgNVBAoTD015IFRydXN0ZWQgSW5jLjAeFw0xODA1MTExODU5

+NDJaFw00NTA5MjUxODU5NDJaMC4xEjAQBgNVBAMTCW15LWNhLm9yZzEYMBYGA1UE

+ChMPTXkgVHJ1c3RlZCBJbmMuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC

+AQEAjtC+mWl4f1/5a6n8sjFtMJ7TNcMkwxwaTRgEUl1+q/MVPGu9qEwtdjFptGho

+XxMEiHMdUZtBzebj07GVfKzp09KRzyUlUpOoENH+ZNeWCQBQ1FGLmWccNB3NcR0P

+kbnqkRpRGpx91Nai0RRdKUTUynKsBc1SgzISsa0x/eKi1xht4blRX20z2ieb0HLl

+A3As4PCVaA4zf//pT2G25K8nWpQqGYKsWzCnA1fWZZeg9o+EgJiTefHFl92myiyX

+j1ClqcpxKeewIIyFXPtGsnht7H3WZ47dZ4eoeBAeT7b16OmW+FmFw/CktRWMF1vl

+KVfMLWp3uBuWvX0/EkeWYIAohQIDAQABozIwMDAPBgNVHRMBAf8EBTADAQH/MB0G

+A1UdDgQWBBS6YoKEP39UxVgaoMYECfk7+zcevTANBgkqhkiG9w0BAQsFAAOCAQEA

+RzBVtc2Yz2t7qG7MKNVz90JhfDOdH3gzB0IwB7NCFxWDfA3zOqP0Ux4K1/jH52zf

+Abdp+XP4NHbgMei3dU/ASqgmCodNoZS7+fMuKMfj/tgPhTN4Xcn8nAIgYobbQ3EI

+xLdwpcNpQq4STsIBtc0FPV06qjgPNBeKLvlPTi8b31/5PMqVbCXjV+tVwxRPUsqC

+MY5kbs81P/0cdUkWYszV5gJybkcNNvHnrgyG1DOfkAnoxtsUGVpmkd1bjy69FAtD

+HBDHgi+cTGgFo/3mdlcv4hisIQpoiLpNi14bOYkrWEjGhVRPGsTDv7MuNPq6/ts1

+czqytnKLhWcMkaoOrP+8uA==
+-----END CERTIFICATE-----
diff --git a/protonj2-client/src/test/resources/client-jceks.keystore b/protonj2-client/src/test/resources/client-jceks.keystore
new file mode 100644
index 0000000..a9449d1
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-jceks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client-jceks.truststore b/protonj2-client/src/test/resources/client-jceks.truststore
new file mode 100644
index 0000000..4a40ca3
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-jceks.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client-jks.keystore b/protonj2-client/src/test/resources/client-jks.keystore
new file mode 100644
index 0000000..d201138
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-jks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client-jks.truststore b/protonj2-client/src/test/resources/client-jks.truststore
new file mode 100644
index 0000000..a8fb225
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-jks.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client-multiple-keys-jks.keystore b/protonj2-client/src/test/resources/client-multiple-keys-jks.keystore
new file mode 100644
index 0000000..2e55ed9
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-multiple-keys-jks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client-pkcs12.keystore b/protonj2-client/src/test/resources/client-pkcs12.keystore
new file mode 100644
index 0000000..3c9efe4
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-pkcs12.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client-pkcs12.truststore b/protonj2-client/src/test/resources/client-pkcs12.truststore
new file mode 100644
index 0000000..78864eb
--- /dev/null
+++ b/protonj2-client/src/test/resources/client-pkcs12.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client.crt b/protonj2-client/src/test/resources/client.crt
new file mode 100644
index 0000000..02ad220
--- /dev/null
+++ b/protonj2-client/src/test/resources/client.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgIEHUknlzANBgkqhkiG9w0BAQsFADAuMRIwEAYDVQQDEwlt

+eS1jYS5vcmcxGDAWBgNVBAoTD015IFRydXN0ZWQgSW5jLjAeFw0xODA1MTExODU5

+NDdaFw00NTA5MjUxODU5NDdaMCIxDzANBgNVBAMTBmNsaWVudDEPMA0GA1UEChMG

+Q2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw10jxMLYCYX1

+6hLqcetHaLu2CrWUJQJseGvIRaN4ygYmhn/OMOnYggxSlKPj8N1NpRk+LtLbX6uE

+2UdTSZUtMbDZybfmITT0OVMyn0vCAOes0XJm1OLX/0aO58Hr15gKBNjcm8fR8Hrr

+ctH5fB90hCWuMW3aN3zzaWRwSwqnGLkMs2W0XQ3yyS/uaaGIOP8kPyTSw3Ew9wvx

+GmOoTFpmvyuwIiuNx643h85sFU1bzrfEC/6YeWj/sMu0xWwYSPpyPI6sjkvGs9Nv

+bdtOQ1Ll1khpaWBZhFzYrbFW4YdOdWhM/7sSNlgal0n0FxC3rqVVYml+9mdwlNlB

+cOSNfFclxwIDAQABo2IwYDAfBgNVHSMEGDAWgBS6YoKEP39UxVgaoMYECfk7+zce

+vTAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBRZ47b/

+Q1xoRGgF+tNMC4q9K3WNgDANBgkqhkiG9w0BAQsFAAOCAQEAaQhrfguZFso0OBWa

+ZkYY72OX+hN7d2Fe2BT9yjlAF67K88tUwHKgjcNekEn9yjBbra0/xKlEZNtFH1ws

+7JOvVQCf+9AxfukDnOKnM6zwh6jpsRdFL92WUllh6GBvkpCYq6uDs4zNdpML0Bcj

+34r7+EwCQrscRrGo+A9OxpdwHEVarUriiAtg9JrPXxMY2KfE7okJzntg9SJhsDAp

+Cgz8elxNhC3Fsd+xqAhGSYwQPhwotj7uG8XIMeSOpSlEXPQ5SMEXX4UVfcyNf3vb

+HS+ARu8d+i5NhpUXCBLhN5oZYik5pVH84mpKpiOug2uRzW+FsW/9oHoI7TyFakdp

+p7K1EA==
+-----END CERTIFICATE-----
diff --git a/protonj2-client/src/test/resources/client.csr b/protonj2-client/src/test/resources/client.csr
new file mode 100644
index 0000000..4c782eb
--- /dev/null
+++ b/protonj2-client/src/test/resources/client.csr
@@ -0,0 +1,16 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIIClzCCAX8CAQAwIjEPMA0GA1UEAxMGY2xpZW50MQ8wDQYDVQQKEwZDbGllbnQw

+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDXSPEwtgJhfXqEupx60do

+u7YKtZQlAmx4a8hFo3jKBiaGf84w6diCDFKUo+Pw3U2lGT4u0ttfq4TZR1NJlS0x

+sNnJt+YhNPQ5UzKfS8IA56zRcmbU4tf/Ro7nwevXmAoE2Nybx9Hweuty0fl8H3SE

+Ja4xbdo3fPNpZHBLCqcYuQyzZbRdDfLJL+5poYg4/yQ/JNLDcTD3C/EaY6hMWma/

+K7AiK43HrjeHzmwVTVvOt8QL/ph5aP+wy7TFbBhI+nI8jqyOS8az029t205DUuXW

+SGlpYFmEXNitsVbhh051aEz/uxI2WBqXSfQXELeupVViaX72Z3CU2UFw5I18VyXH

+AgMBAAGgMDAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBRZ47b/Q1xoRGgF+tNM

+C4q9K3WNgDANBgkqhkiG9w0BAQsFAAOCAQEAcL/z/ROInGeWXIJ6N8eafGprWYNY

+oKi+TO0U27ikYexngmQE3r4j8HF4HP33AjZG+GvcVpk7CUXxW9V1dGODsTxcTF5L

+R/dzmr79AuHMDFCTe71VWDzDK4bBiqL8LkmL+hdHCzD6IJpXor1Y7uvwDWMsIxhE

+nMvX0o347TvSLaF+/V7kCnOdWGtgKBWr8uL7Xkva3c3RvjSdzHjBocv3hlPuqLL6

++nF3FwdZSw+xzEOBwb20uhqWGSCtjkSnvehaVssHNuUCdU+RKwBuoYA+JAIFjK4m

+ofsY0UIFoyzFDDczQjzreu5GfKepHPfkcv5ASYeV9tPT1qErqzvOf3Di4g==
+-----END NEW CERTIFICATE REQUEST-----
diff --git a/protonj2-client/src/test/resources/client2-jks.keystore b/protonj2-client/src/test/resources/client2-jks.keystore
new file mode 100644
index 0000000..95ea59f
--- /dev/null
+++ b/protonj2-client/src/test/resources/client2-jks.keystore
Binary files differ
diff --git a/protonj2-client/src/test/resources/client2.crt b/protonj2-client/src/test/resources/client2.crt
new file mode 100644
index 0000000..eff5f68
--- /dev/null
+++ b/protonj2-client/src/test/resources/client2.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDMjCCAhqgAwIBAgIEZWnrozANBgkqhkiG9w0BAQsFADAuMRIwEAYDVQQDEwlt

+eS1jYS5vcmcxGDAWBgNVBAoTD015IFRydXN0ZWQgSW5jLjAeFw0xODA1MTExODU5

+NTBaFw00NTA5MjUxODU5NTBaMCQxEDAOBgNVBAMTB2NsaWVudDIxEDAOBgNVBAoT

+B0NsaWVudDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpKBMgWQub

+gEPY13utqlX1Pjgvkh6xbw8DQHApCWVIP+axC6j2rxlwpHOWMMmnIgw2Xg4RuhJF

+pKPdi7n/ROw/o5sGWuHPb9PdOc/WF5XEyiIyWIvEZlQ4TNtAGlj2dle9CuTniDKU

+88dVIX4f8g9CVjbfGioiJrrqe+gwMd4QxHk0IwnTMV/Yjimbm2jwwK9PCQRiL5Sx

+5hUc5ldMZAzbMHmljcVhLvUp1zdVgWfOZwHxZ95gRjOyqmwwvx5LH/gM/WEYHORG

+9u3j1u9lmynn9Hd/C5JNxVW4ZicuKMcIOZdWq06rUPuaBbecJbJmyCJSjnoAn0Lq

+6ztDz0XO2jCdAgMBAAGjYjBgMB8GA1UdIwQYMBaAFLpigoQ/f1TFWBqgxgQJ+Tv7

+Nx69MAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFBto

+WzSMahL9EfruTziGk2PrQ7ldMA0GCSqGSIb3DQEBCwUAA4IBAQBZGmTQTedrQG6h

+A4UbCRhNh/fvUZ1jIHOM8fsDhKnzsDeYRL24CuQWhIwAn0vQ6ShwBvoQLrJtweJA

+PqKJjdYSTBESf/Mq0kkHa/T/AfhBc5EW4/k+6yL6No2OAZp9R9tBHTYq1AexDZoZ

+eT4reEW2iIX2Iv1HYJd8TkIF9lsAQ2DYBg6484h5HR0i4aHW+oCT69Tj/6ce7Gwm

+HfsCPz23aI7enlylhDzcYHt86QkHvm24c4cVhJ2rvY1/SYh0AJUpaG542EYXieNn

+wiylEgiUegXrXJ9SaT7/DFbCGz9mA505oEpSWuM7+lD6Wnwi2cYk/rXZfB83oypU

+xaSUq6ez
+-----END CERTIFICATE-----
diff --git a/protonj2-client/src/test/resources/client2.csr b/protonj2-client/src/test/resources/client2.csr
new file mode 100644
index 0000000..f92e8d6
--- /dev/null
+++ b/protonj2-client/src/test/resources/client2.csr
@@ -0,0 +1,16 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIICmTCCAYECAQAwJDEQMA4GA1UEAxMHY2xpZW50MjEQMA4GA1UEChMHQ2xpZW50

+MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkoEyBZC5uAQ9jXe62q

+VfU+OC+SHrFvDwNAcCkJZUg/5rELqPavGXCkc5YwyaciDDZeDhG6EkWko92Luf9E

+7D+jmwZa4c9v0905z9YXlcTKIjJYi8RmVDhM20AaWPZ2V70K5OeIMpTzx1Uhfh/y

+D0JWNt8aKiImuup76DAx3hDEeTQjCdMxX9iOKZubaPDAr08JBGIvlLHmFRzmV0xk

+DNsweaWNxWEu9SnXN1WBZ85nAfFn3mBGM7KqbDC/Hksf+Az9YRgc5Eb27ePW72Wb

+Kef0d38Lkk3FVbhmJy4oxwg5l1arTqtQ+5oFt5wlsmbIIlKOegCfQurrO0PPRc7a

+MJ0CAwEAAaAwMC4GCSqGSIb3DQEJDjEhMB8wHQYDVR0OBBYEFBtoWzSMahL9Efru

+TziGk2PrQ7ldMA0GCSqGSIb3DQEBCwUAA4IBAQBgsuXYf1McupAYqWarWuWPaNGJ

+NkCSQCtF3KrUe9foGS/OVKxXksMeTfYUuO5RFXPT3jGtsSNNgNuVG6pg8zX82W9d

+bZq9bGAZXJeRVtnz8+9m1/1GB+q0xHYz2SyYXq2QdqNLFf9fmNQTiKr+R5yO710S

+OvmXbG7CsRBLOqASud3Pb7uAl5x+tmD/Po7E0DNY0eGK0oMDfgOe8FFPmfMN9HKT

+Pv1+KMHkjMr1sKxXogowTjqidokM53AyTRDODyT+1mWbumRhupVblGLcQQ/EbKAZ

+cU7B+o1johWoPgVC38r0aRVgqMPDNLFibtKFoAkZTDDPK2PB0vuNjIdGSRav
+-----END NEW CERTIFICATE REQUEST-----
diff --git a/protonj2-client/src/test/resources/log4j2-test.properties b/protonj2-client/src/test/resources/log4j2-test.properties
new file mode 100644
index 0000000..0fc0d65
--- /dev/null
+++ b/protonj2-client/src/test/resources/log4j2-test.properties
@@ -0,0 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name=ClientModuleTestPropertiesConfig
+status=warn
+
+appender.console.type=Console
+appender.console.name=STDOUT
+appender.console.layout.type=PatternLayout
+appender.console.layout.pattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+
+rootLogger.level=debug
+rootLogger.appenderRef.console.ref=STDOUT
+
+logger.client.name=org.apache.qpid.protonj2.client
+logger.client.level=trace
+
+logger.proton.name=org.apache.qpid.protonj2
+logger.proton.level=trace
+
+
diff --git a/protonj2-client/src/test/resources/minikdc-krb5.conf b/protonj2-client/src/test/resources/minikdc-krb5.conf
new file mode 100644
index 0000000..9645dec
--- /dev/null
+++ b/protonj2-client/src/test/resources/minikdc-krb5.conf
@@ -0,0 +1,26 @@
+#
+# 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.
+#
+[libdefaults]
+    default_realm = {0}
+    udp_preference_limit = 1
+    default_keytab_name = FILE:target/SaslGssApiIntegrationTest.krb5.keytab
+
+[realms]
+    {0} = '{'
+        kdc = {1}:{2}
+    '}'
\ No newline at end of file
diff --git a/protonj2-client/src/test/resources/other-ca-jks.truststore b/protonj2-client/src/test/resources/other-ca-jks.truststore
new file mode 100644
index 0000000..d3ce8f4
--- /dev/null
+++ b/protonj2-client/src/test/resources/other-ca-jks.truststore
Binary files differ
diff --git a/protonj2-client/src/test/resources/other-ca.crt b/protonj2-client/src/test/resources/other-ca.crt
new file mode 100644
index 0000000..56c650b
--- /dev/null
+++ b/protonj2-client/src/test/resources/other-ca.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDGDCCAgCgAwIBAgIEUosGfTANBgkqhkiG9w0BAQsFADA0MRUwEwYDVQQDEwxv

+dGhlci1jYS5vcmcxGzAZBgNVBAoTEk90aGVyIFRydXN0ZWQgSW5jLjAeFw0xODA1

+MTExODU5NTFaFw00NTA5MjUxODU5NTFaMDQxFTATBgNVBAMTDG90aGVyLWNhLm9y

+ZzEbMBkGA1UEChMST3RoZXIgVHJ1c3RlZCBJbmMuMIIBIjANBgkqhkiG9w0BAQEF

+AAOCAQ8AMIIBCgKCAQEA0HR4+XIBarxaYb4622xg5RbRivIQQAJqIK5Yq2r1ydXp

+lNUPCIhwOASISVnstgZmS+E+4BqwJU74Q/9Ve6xF4I6zAvkHeMiXmuH/8Nz5gLtK

+FZp4y8jBBQC0MvwMVd5P6fDZDB87k0LtR8qmBqII2xScHNDMeN89d9pCPnSqHg8L

+3DADUAx2WPg9GIQfoaKHim49IaG1EpusIbhl6Qi6n+zRaspDoYeyatmpaQRvHFfP

+c86lLLe51kzw1emgndpMiV5GjWZbLFEX7iKkuQM4MutlyqMTChsANZ8zZMJuxBVs

+vW55R+plBI9nF3bwaS+8PxziWcRyY8hmN39IzMDUTQIDAQABozIwMDAPBgNVHRMB

+Af8EBTADAQH/MB0GA1UdDgQWBBSQaiIc4oosj4KQqUluHQsw+cIWODANBgkqhkiG

+9w0BAQsFAAOCAQEAfSpaNcDokG8BSdydHkE5Fwf7vzOobr4RSmR6zeEeAkzqMh0C

+b8X6YPXrd9xT2mTTHqbEXF7FbHVNGjuOeQStQVQc7OfYL25E1VqHbctfjsGEJxO+

+JJT0i9mpoaovSSkoAbS0I3mwJVRNVjxfaBlNTkxuMu/lbYMm3evknqUnN6g81W1W

+thXAORb/CSQFoAT52cq3aHlVj/VIwoHlMYEAC56Sbn/CvAnV8NjTwscGdzN6Hx55

+V+ONEflVr4cZHftW//T9aSn0F6zjHZTVmYeCHYOrH5pIMaCuR6a41LNVo344Y/SX

+KaUUn7wCaxg4aXAl1worNCqEwSk7+wqgmdRnKg==
+-----END CERTIFICATE-----
diff --git a/protonj2-performance-tests/README.md b/protonj2-performance-tests/README.md
new file mode 100644
index 0000000..edfaf1f
--- /dev/null
+++ b/protonj2-performance-tests/README.md
@@ -0,0 +1,55 @@
+protonj2 JMH Benchmarks
+-------
+This module contains optional [JMH](http://openjdk.java.net/projects/code-tools/jmh/) performance tests designed
+to stress performance critical parts of the proton engine (eg encoders, decoders).
+
+Note that this module is an optional part of the overall project build and does not deploy anything, due to its use
+of JMH which is not permissively licensed. The module must either be built directly, or enabled
+within the overall build by using the 'performance-jmh' maven profile.
+
+Building the benchmarks
+-------
+
+TODO - Currently just part of the build with deploy disabled.
+
+The benchmarks are maven built and involve some code generation for the JMH part. As such it is required that you
+rebuild upon changing the code. As the codebase is small it is recommended that you do this from the project
+root folder to avoid missing any changes from other modules.
+
+As noted above this module is optional in the main build, enabled by the performance-jmh profile, so to enable it
+a command such as the following can be used from the root folder:
+
+    mvn clean install -Pperformance-jmh
+
+Running the benchmarks: General
+-------
+It is recommended that you consider some basic benchmarking practices before running benchmarks:
+
+ 1. Use a quiet machine with enough CPUs to run the number of threads you mean to run.
+ 2. Set the CPU freq to avoid variance due to turbo boost/heating.
+ 3. Use an OS tool such as taskset to pin the threads in the topology you mean to measure.
+
+Running the JMH Benchmarks
+-----
+To run all JMH benchmarks:
+
+    java -jar target/protonj2-performance-tests.jar -f <number-of-forks> -wi <number-of-warmup-iterations> -i <number-of-iterations>
+To list available benchmarks:
+
+    java -jar target/protonj2-performance-tests.jar -l
+Some JMH help:
+
+    java -jar target/protonj2-performance-tests.jar -h
+
+Example
+-----
+To run a benchmark on the String decoding while saving the results in json format:
+
+    java -jar target/protonj2-performance-tests.jar StringsBenchmark.decode* -f 1 -wi 5 -i 5 -rf json -rff strings_decode_before.json -gc true
+
+After changing something in the String decode path and building the whole project again,
+another snapshot of the current state of performance for the same case can be taken:
+
+    java -jar target/protonj2-performance-tests.jar StringsBenchmark.decode* -f 1 -wi 5 -i 5 -rf json -rff strings_decode_after.json -gc true
+
+then it is possible to use many graphical tools to compare the results: one is [JMH Visualizer](http://jmh.morethan.io/).
diff --git a/protonj2-performance-tests/pom.xml b/protonj2-performance-tests/pom.xml
new file mode 100644
index 0000000..2f531a8
--- /dev/null
+++ b/protonj2-performance-tests/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<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>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>protonj2-performance-tests</artifactId>
+  <name>Qpid protonj2 JMH performance tests</name>
+  <description>JMH Tests for the protonj2 library</description>
+  <packaging>jar</packaging>
+
+  <properties>
+    <jmh-version>1.29</jmh-version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.openjdk.jmh</groupId>
+      <artifactId>jmh-core</artifactId>
+      <version>${jmh-version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.openjdk.jmh</groupId>
+      <artifactId>jmh-generator-annprocess</artifactId>
+      <version>${jmh-version}</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <finalName>protonj2-performance-tests</finalName>
+              <transformers>
+                <transformer
+                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <mainClass>org.openjdk.jmh.Main</mainClass>
+                </transformer>
+              </transformers>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- Don't deploy artifacts for this module. It has non-permissive dependencies and is only
+           optionally used for local testing. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
\ No newline at end of file
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/CodecBenchmarkBase.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/CodecBenchmarkBase.java
new file mode 100644
index 0000000..d26804a
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/CodecBenchmarkBase.java
@@ -0,0 +1,85 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.profile.GCProfiler;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+public abstract class CodecBenchmarkBase {
+
+    public static final int DEFAULT_BUFFER_SIZE = 8192;
+
+    protected ProtonBuffer buffer;
+    protected Encoder encoder = CodecFactory.getDefaultEncoder();
+    protected EncoderState encoderState = encoder.newEncoderState();
+    protected Decoder decoder = CodecFactory.getDefaultDecoder();
+    protected DecoderState decoderState = decoder.newDecoderState();
+
+    /**
+     * It could be overridden to allow encoding/decoding buffer to be sized
+     * differently from {@link #DEFAULT_BUFFER_SIZE}
+     *
+     * @return the buffer size to use in tests when creating encoding buffers.
+     */
+    protected int bufferSize() {
+        return DEFAULT_BUFFER_SIZE;
+    }
+
+    private void initProtonBuffer() {
+        buffer = ProtonByteBufferAllocator.DEFAULT.allocate(bufferSize());
+    }
+
+    public void init() {
+        initProtonBuffer();
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(CodecBenchmarkBase.class);
+    }
+
+    public static void runBenchmark(Class<?> benchmarkClass) throws RunnerException {
+        final Options opt = new OptionsBuilder()
+            .include(benchmarkClass.getSimpleName())
+            .addProfiler(GCProfiler.class)
+            .shouldDoGC(true)
+            .warmupIterations(5)
+            .measurementIterations(5)
+            .forks(1)
+            .build();
+
+        new Runner(opt).run();
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/ApplicationPropertiesBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/ApplicationPropertiesBenchmark.java
new file mode 100644
index 0000000..2f93a1e
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/ApplicationPropertiesBenchmark.java
@@ -0,0 +1,70 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class ApplicationPropertiesBenchmark extends CodecBenchmarkBase {
+
+    private Blackhole blackhole;
+    private ApplicationProperties properties;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        super.init();
+        this.blackhole = blackhole;
+        initApplicationProperties();
+        encode();
+    }
+
+    private void initApplicationProperties() {
+        properties = new ApplicationProperties(new HashMap<String, Object>());
+        properties.getValue().put("test1", UnsignedByte.valueOf((byte) 128));
+        properties.getValue().put("test2", UnsignedShort.valueOf((short) 128));
+        properties.getValue().put("test3", UnsignedInteger.valueOf((byte) 128));
+    }
+
+    @Benchmark
+    public ProtonBuffer encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, properties);
+        return buffer;
+    }
+
+    @Benchmark
+    public ProtonBuffer decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+        return buffer;
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(ApplicationPropertiesBenchmark.class);
+    }
+}
\ No newline at end of file
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/DataBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/DataBenchmark.java
new file mode 100644
index 0000000..d893b66
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/DataBenchmark.java
@@ -0,0 +1,69 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class DataBenchmark extends CodecBenchmarkBase {
+
+    private Blackhole blackhole;
+    private Data data1;
+    private Data data2;
+    private Data data3;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initData();
+        encode();
+    }
+
+    private void initData() {
+        data1 = new Data(new Binary(new byte[] { 1, 2, 3 }));
+        data2 = new Data(new Binary(new byte[] { 4, 5, 6 }));
+        data3 = new Data(new Binary(new byte[] { 7, 8, 9 }));
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, data1);
+        encoder.writeObject(buffer, encoderState, data2);
+        encoder.writeObject(buffer, encoderState, data3);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(DataBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/DispositionBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/DispositionBenchmark.java
new file mode 100644
index 0000000..e1fbaae
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/DispositionBenchmark.java
@@ -0,0 +1,66 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class DispositionBenchmark extends CodecBenchmarkBase {
+    private Disposition disposition;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initDisposition();
+        encode();
+    }
+
+    private void initDisposition() {
+        disposition = new Disposition();
+        disposition.setRole(Role.RECEIVER);
+        disposition.setSettled(true);
+        disposition.setState(Accepted.getInstance());
+        disposition.setFirst(2);
+        disposition.setLast(2);
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, disposition);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(DispositionBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/FlowBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/FlowBenchmark.java
new file mode 100644
index 0000000..78de6e4
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/FlowBenchmark.java
@@ -0,0 +1,68 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class FlowBenchmark extends CodecBenchmarkBase {
+
+    private Flow flow;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initFlow();
+        encode();
+    }
+
+    private void initFlow() {
+        flow = new Flow();
+        flow.setNextIncomingId(1);
+        flow.setIncomingWindow(2047);
+        flow.setNextOutgoingId(1);
+        flow.setOutgoingWindow(UnsignedInteger.MAX_VALUE.longValue());
+        flow.setHandle(0);
+        flow.setDeliveryCount(10);
+        flow.setLinkCredit(1000);
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, flow);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(FlowBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/HeaderBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/HeaderBenchmark.java
new file mode 100644
index 0000000..af3b1dd
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/HeaderBenchmark.java
@@ -0,0 +1,62 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class HeaderBenchmark extends CodecBenchmarkBase {
+
+    private Header header;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initHeader();
+        encode();
+    }
+
+    private void initHeader() {
+        header = new Header();
+        header.setDurable(true);
+        header.setFirstAcquirer(true);
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, header);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(HeaderBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/MessageAnnotationsBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/MessageAnnotationsBenchmark.java
new file mode 100644
index 0000000..6bfdd2d
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/MessageAnnotationsBenchmark.java
@@ -0,0 +1,68 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class MessageAnnotationsBenchmark extends CodecBenchmarkBase {
+
+    private MessageAnnotations annotations;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initMessageAnnotations();
+        encode();
+    }
+
+    private void initMessageAnnotations() {
+        annotations = new MessageAnnotations(new HashMap<Symbol, Object>());
+        annotations.getValue().put(Symbol.valueOf("test1"), UnsignedByte.valueOf((byte) 128));
+        annotations.getValue().put(Symbol.valueOf("test2"), UnsignedShort.valueOf((short) 128));
+        annotations.getValue().put(Symbol.valueOf("test3"), UnsignedInteger.valueOf((byte) 128));
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, annotations);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(MessageAnnotationsBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/PropertiesBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/PropertiesBenchmark.java
new file mode 100644
index 0000000..2616305
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/PropertiesBenchmark.java
@@ -0,0 +1,63 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class PropertiesBenchmark extends CodecBenchmarkBase {
+
+    private Properties properties;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initProperties();
+        encode();
+    }
+
+    private void initProperties() {
+        properties = new Properties();
+        properties.setTo("queue:1");
+        properties.setMessageId("ID:Message:1");
+        properties.setCreationTime(System.currentTimeMillis());
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, properties);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(PropertiesBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/TransferBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/TransferBenchmark.java
new file mode 100644
index 0000000..002b8a0
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/messaging/TransferBenchmark.java
@@ -0,0 +1,64 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class TransferBenchmark extends CodecBenchmarkBase {
+
+    private Transfer transfer;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initTransfer();
+        encode();
+    }
+
+    private void initTransfer() {
+        transfer = new Transfer();
+        transfer.setDeliveryTag(new byte[] { 1, 2, 3 });
+        transfer.setHandle(10);
+        transfer.setMessageFormat(UnsignedInteger.ZERO.intValue());
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeObject(buffer, encoderState, transfer);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readObject(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(TransferBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/ListOfIntBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/ListOfIntBenchmark.java
new file mode 100644
index 0000000..9a4f728
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/ListOfIntBenchmark.java
@@ -0,0 +1,65 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class ListOfIntBenchmark extends CodecBenchmarkBase {
+
+    private static final int LIST_SIZE = 10;
+    private ArrayList<Object> listOfInts;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initListOfInts();
+        encode();
+    }
+
+    private void initListOfInts() {
+        this.listOfInts = new ArrayList<>(LIST_SIZE);
+        for (int i = 0; i < LIST_SIZE; i++) {
+            listOfInts.add(i);
+        }
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeList(buffer, encoderState, listOfInts);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readList(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(ListOfIntBenchmark.class);
+    }
+
+}
\ No newline at end of file
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/StringBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/StringBenchmark.java
new file mode 100644
index 0000000..6276a44
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/StringBenchmark.java
@@ -0,0 +1,84 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class StringBenchmark extends CodecBenchmarkBase {
+
+    private static final String PAYLOAD =
+          "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+        + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+        + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+        + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+        + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
+
+    private Blackhole blackhole;
+    private String string1;
+    private String string2;
+    private String string3;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initStrings();
+        encode();
+    }
+
+    private void initStrings() {
+        string1 = new String("String-1");
+        string2 = new String("String-2");
+        string3 = new String("String-3");
+    }
+
+    @Benchmark
+    public ProtonBuffer encode() {
+        buffer.clear();
+        encoder.writeString(buffer, encoderState, string1);
+        encoder.writeString(buffer, encoderState, string2);
+        encoder.writeString(buffer, encoderState, string3);
+        return buffer;
+    }
+
+    @Benchmark
+    public ProtonBuffer encodeLargeString() {
+        buffer.clear();
+        encoder.writeString(buffer, encoderState, PAYLOAD);
+        return buffer;
+    }
+
+    @Benchmark
+    public ProtonBuffer decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readString(buffer, decoderState));
+        blackhole.consume(decoder.readString(buffer, decoderState));
+        blackhole.consume(decoder.readString(buffer, decoderState));
+        return buffer;
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(StringBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/SymbolBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/SymbolBenchmark.java
new file mode 100644
index 0000000..7cca4b9
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/SymbolBenchmark.java
@@ -0,0 +1,68 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class SymbolBenchmark extends CodecBenchmarkBase {
+
+    private Symbol symbol1;
+    private Symbol symbol2;
+    private Symbol symbol3;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initSymbols();
+        encode();
+    }
+
+    private void initSymbols() {
+        symbol1 = Symbol.valueOf("Symbol-1");
+        symbol2 = Symbol.valueOf("Symbol-2");
+        symbol3 = Symbol.valueOf("Symbol-3");
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeSymbol(buffer, encoderState, symbol1);
+        encoder.writeSymbol(buffer, encoderState, symbol2);
+        encoder.writeSymbol(buffer, encoderState, symbol3);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readSymbol(buffer, decoderState));
+        blackhole.consume(decoder.readSymbol(buffer, decoderState));
+        blackhole.consume(decoder.readSymbol(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(SymbolBenchmark.class);
+    }
+}
\ No newline at end of file
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/UUIDBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/UUIDBenchmark.java
new file mode 100644
index 0000000..4417ce5
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/codec/primitives/UUIDBenchmark.java
@@ -0,0 +1,60 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.codec.CodecBenchmarkBase;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+public class UUIDBenchmark extends CodecBenchmarkBase {
+
+    private UUID uuid;
+    private Blackhole blackhole;
+
+    @Setup
+    public void init(Blackhole blackhole) {
+        this.blackhole = blackhole;
+        super.init();
+        initUUID();
+        encode();
+    }
+
+    private void initUUID() {
+        this.uuid = UUID.randomUUID();
+    }
+
+    @Benchmark
+    public void encode() {
+        buffer.clear();
+        encoder.writeUUID(buffer, encoderState, uuid);
+    }
+
+    @Benchmark
+    public void decode() throws IOException {
+        buffer.setReadIndex(0);
+        blackhole.consume(decoder.readUUID(buffer, decoderState));
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(UUIDBenchmark.class);
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/LinkedHashMapBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/LinkedHashMapBenchmark.java
new file mode 100644
index 0000000..b2dbc3a
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/LinkedHashMapBenchmark.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.openjdk.jmh.runner.RunnerException;
+
+/**
+ * Comparison Benchmark for the Java {@link LinkedHashMap} to compare against internal implementations
+ */
+public class LinkedHashMapBenchmark extends MapBenchmarkBase {
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(LinkedHashMapBenchmark.class);
+    }
+
+    @Override
+    protected Map<UnsignedInteger, String> createMap() {
+        return new LinkedHashMap<>();
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/MapBenchmarkBase.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/MapBenchmarkBase.java
new file mode 100644
index 0000000..712f26f
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/MapBenchmarkBase.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.profile.GCProfiler;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+/**
+ * Base for benchmarks involving {@link Map} types.
+ */
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 1)
+public abstract class MapBenchmarkBase {
+
+    public static final int DEFAULT_MAP_VALUE_RANGE = 8192;
+
+    protected final String DUMMY_STRING = "ASDFGHJ";
+    protected final Random random = new Random();
+
+    protected Map<UnsignedInteger, String> map;
+    protected Map<UnsignedInteger, String> filledMap;
+
+    @Setup
+    public void init() {
+        this.random.setSeed(System.currentTimeMillis());
+        this.map = createMap();
+        this.filledMap = fillMap(createMap());
+    }
+
+    @Benchmark
+    public void put() {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            map.put(UnsignedInteger.valueOf(i), DUMMY_STRING);
+        }
+    }
+
+    @Benchmark
+    public void get(Blackhole blackHole) {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            blackHole.consume(filledMap.get(UnsignedInteger.valueOf(i)));
+        }
+    }
+
+    @Benchmark
+    public void remove(Blackhole blackHole) {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            blackHole.consume(filledMap.remove(UnsignedInteger.valueOf(i)));
+        }
+    }
+
+    @Benchmark
+    public void produceAndConsume(Blackhole blackHole) {
+        for (int i = 0; i < 32; ++i) {
+            map.put(UnsignedInteger.valueOf(i), DUMMY_STRING);
+        }
+
+        for (int p = 0, c = map.size(); p < DEFAULT_MAP_VALUE_RANGE; ++p, ++c) {
+            blackHole.consume(filledMap.put(UnsignedInteger.valueOf(p), DUMMY_STRING));
+            blackHole.consume(filledMap.remove(UnsignedInteger.valueOf(c)));
+        }
+    }
+
+    @Benchmark
+    public void randomProduceAndConsume(Blackhole blackHole) {
+        for (int i = 0; i < 32; ++i) {
+            map.put(UnsignedInteger.valueOf(i), DUMMY_STRING);
+        }
+
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            int p = random.nextInt(DEFAULT_MAP_VALUE_RANGE);
+            int c = random.nextInt(DEFAULT_MAP_VALUE_RANGE);
+
+            blackHole.consume(filledMap.put(UnsignedInteger.valueOf(p), DUMMY_STRING));
+            blackHole.consume(filledMap.remove(UnsignedInteger.valueOf(c)));
+        }
+    }
+
+    protected abstract Map<UnsignedInteger, String> createMap();
+
+    protected Map<UnsignedInteger, String> fillMap(Map<UnsignedInteger, String> target) {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            target.put(UnsignedInteger.valueOf(i), DUMMY_STRING);
+        }
+
+        return target;
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(MapBenchmarkBase.class);
+    }
+
+    public static void runBenchmark(Class<?> benchmarkClass) throws RunnerException {
+        final Options opt = new OptionsBuilder()
+            .include(benchmarkClass.getSimpleName())
+            .addProfiler(GCProfiler.class)
+            .shouldDoGC(true)
+            .warmupIterations(5)
+            .measurementIterations(5)
+            .forks(1)
+            .build();
+
+        new Runner(opt).run();
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/RingQueueBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/RingQueueBenchmark.java
new file mode 100644
index 0000000..7d61483
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/RingQueueBenchmark.java
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.profile.GCProfiler;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+/**
+ * Base for benchmarks involving {@link Map} types.
+ */
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 1)
+public class RingQueueBenchmark {
+
+    public static final int DEFAULT_MAP_VALUE_RANGE = 8192;
+
+    protected final String DUMMY_STRING = "ASDFGHJ";
+    protected final Random random = new Random();
+
+    @Setup
+    public void init() {
+        this.random.setSeed(System.currentTimeMillis());
+    }
+
+    @Benchmark
+    public void offer() {
+        final Queue<String> queue = new RingQueue<>(32);
+
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            if (!queue.offer(DUMMY_STRING)) {
+                queue.clear();
+                queue.offer(DUMMY_STRING);
+            }
+        }
+    }
+
+    @Benchmark
+    public void produceAndConsume(Blackhole blackHole) {
+        final Queue<String> queue = new RingQueue<>(32);
+
+        for (int i = 0; i < 32; ++i) {
+            queue.offer(DUMMY_STRING);
+        }
+
+        for (int p = 0; p < DEFAULT_MAP_VALUE_RANGE; ++p) {
+            blackHole.consume(queue.poll());
+            queue.offer(DUMMY_STRING);
+        }
+    }
+
+    @Benchmark
+    public void contains(Blackhole blackHole) {
+        final Queue<Integer> queue = new RingQueue<>(256);
+
+        for (int i = 0; i < 256; ++i) {
+            queue.offer(Integer.valueOf(i));
+        }
+
+        for (int i = 0; i < 256; ++i) {
+            blackHole.consume(queue.contains(Integer.valueOf(i)));
+        }
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(RingQueueBenchmark.class);
+    }
+
+    public static void runBenchmark(Class<?> benchmarkClass) throws RunnerException {
+        final Options opt = new OptionsBuilder()
+            .include(benchmarkClass.getSimpleName())
+            .addProfiler(GCProfiler.class)
+            .shouldDoGC(true)
+            .warmupIterations(5)
+            .measurementIterations(5)
+            .forks(1)
+            .build();
+
+        new Runner(opt).run();
+    }
+}
diff --git a/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/SplayMapBenchmark.java b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/SplayMapBenchmark.java
new file mode 100644
index 0000000..994fdee
--- /dev/null
+++ b/protonj2-performance-tests/src/main/java/org/apache/qpid/protonj2/engine/util/SplayMapBenchmark.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.RunnerException;
+
+/**
+ * Tests for performance characteristics of the {@link SplayMap} implementation
+ */
+public class SplayMapBenchmark extends MapBenchmarkBase {
+
+    public static void main(String[] args) throws RunnerException {
+        runBenchmark(SplayMapBenchmark.class);
+    }
+
+    private SplayMap<String> sqMap;
+    private SplayMap<String> sqFilledMap;
+
+    @Override
+    @Setup
+    public void init() {
+        super.init();
+
+        this.sqMap = (SplayMap<String>) map;
+        this.sqFilledMap = (SplayMap<String>) filledMap;
+    }
+
+    @Benchmark
+    public void putWithPrimitive() {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            sqMap.put(i, DUMMY_STRING);
+        }
+    }
+
+    @Benchmark
+    public void getWithPrimitive(Blackhole blackHole) {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            blackHole.consume(sqFilledMap.get(i));
+        }
+    }
+
+    @Benchmark
+    public void removeWithPrimitive(Blackhole blackHole) {
+        for (int i = 0; i < DEFAULT_MAP_VALUE_RANGE; ++i) {
+            blackHole.consume(sqFilledMap.remove(i));
+        }
+    }
+
+    @Override
+    protected Map<UnsignedInteger, String> createMap() {
+        return new SplayMap<>();
+    }
+}
diff --git a/protonj2-test-driver/pom.xml b/protonj2-test-driver/pom.xml
new file mode 100644
index 0000000..83e2d2a
--- /dev/null
+++ b/protonj2-test-driver/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<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>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>protonj2-test-driver</artifactId>
+  <packaging>jar</packaging>
+  <name>Qpid protonj2 AMQP Test Driver</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-buffer</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-handler</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-codec-http</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-library</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>proton-j</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/AMQPTestDriver.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/AMQPTestDriver.java
new file mode 100644
index 0000000..71cd3a5
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/AMQPTestDriver.java
@@ -0,0 +1,557 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.apache.qpid.protonj2.test.driver.actions.ScriptCompleteAction;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.codec.transport.HeartBeat;
+import org.apache.qpid.protonj2.test.driver.codec.transport.PerformativeDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.PerformativeDescribedType.PerformativeType;
+import org.apache.qpid.protonj2.test.driver.exceptions.UnexpectedPerformativeError;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Test driver object used to drive inputs and inspect outputs of an Engine.
+ */
+public class AMQPTestDriver implements Consumer<ByteBuffer> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AMQPTestDriver.class);
+
+    private final String driverName;
+    private final FrameDecoder frameParser;
+    private final FrameEncoder frameEncoder;
+
+    private final DriverSessions sessions = new DriverSessions(this);
+
+    private final Consumer<ByteBuffer> frameConsumer;
+    private final Consumer<AssertionError> assertionConsumer;
+    private final Supplier<ScheduledExecutorService> schedulerSupplier;
+
+    private volatile AssertionError failureCause;
+
+    private int advertisedIdleTimeout = 0;
+
+    private volatile int emptyFrameCount;
+    private volatile int performativeCount;
+    private volatile int saslPerformativeCount;
+
+    private int inboundMaxFrameSize = Integer.MAX_VALUE;
+    private int outboundMaxFrameSize = Integer.MAX_VALUE;
+
+    /**
+     *  Holds the expectations for processing of data from the peer under test.
+     *  Uses a thread safe queue to avoid contention on adding script entries
+     *  and processing incoming data (although you should probably not do that).
+     */
+    private final Queue<ScriptedElement> script = new ArrayDeque<>();
+
+    /**
+     * Create a test driver instance connected to the given Engine instance.
+     *
+     * @param name
+     *      A unique test driver name that should allow log inspection of sends and receiver for a driver instance.
+     * @param frameConsumer
+     *      A {@link Consumer} that will accept encoded frames in ProtonBuffer instances.
+     * @param scheduler
+     *      A {@link Supplier} that will provide this driver with a scheduler service for delayed actions
+     */
+    public AMQPTestDriver(String name, Consumer<ByteBuffer> frameConsumer, Supplier<ScheduledExecutorService> scheduler) {
+        this(name, frameConsumer, null, scheduler);
+    }
+
+    /**
+     * Create a test driver instance connected to the given Engine instance.
+     *
+     * @param name
+     *      A unique test driver name that should allow log inspection of sends and receiver for a driver instance.
+     * @param frameConsumer
+     *      A {@link Consumer} that will accept encoded frames in ProtonBuffer instances.
+     * @param assertionConsumer
+     *      A {@link Consumer} that will handle test assertions from the scripted expectations
+     * @param scheduler
+     *      A {@link Supplier} that will provide this driver with a scheduler service for delayed actions
+     */
+    public AMQPTestDriver(String name, Consumer<ByteBuffer> frameConsumer, Consumer<AssertionError> assertionConsumer, Supplier<ScheduledExecutorService> scheduler) {
+        this.frameConsumer = frameConsumer;
+        this.assertionConsumer = assertionConsumer;
+        this.schedulerSupplier = scheduler;
+        this.driverName = name;
+
+        // Configure test driver resources
+        this.frameParser = new FrameDecoder(this);
+        this.frameEncoder = new FrameEncoder(this);
+    }
+
+    /**
+     * @return the Sessions tracking manager for this driver.
+     */
+    public DriverSessions sessions() {
+        return sessions;
+    }
+
+    /**
+     * @return the assigned name of this AMQP test driver
+     */
+    public Object getName() {
+        return driverName;
+    }
+
+    //----- View the test driver state
+
+    public int getAdvertisedIdleTimeout() {
+        return advertisedIdleTimeout;
+    }
+
+    public void setAdvertisedIdleTimeout(int advertisedIdleTimeout) {
+        this.advertisedIdleTimeout = advertisedIdleTimeout;
+    }
+
+    public int getEmptyFrameCount() {
+        return emptyFrameCount;
+    }
+
+    public int getPerformativeCount() {
+        return performativeCount;
+    }
+
+    public int getSaslPerformativeCount() {
+        return saslPerformativeCount;
+    }
+
+    /**
+     * @return the maximum allowed inbound frame size.
+     */
+    public int getInboundMaxFrameSize() {
+        return inboundMaxFrameSize;
+    }
+
+    public void setInboundMaxFrameSize(int maxSize) {
+        this.inboundMaxFrameSize = maxSize;
+    }
+
+    /**
+     * @return the maximum allowed outbound frame size.
+     */
+    public int getOutboundMaxFrameSize() {
+        return outboundMaxFrameSize;
+    }
+
+    public void setOutboundMaxFrameSize(int maxSize) {
+        this.outboundMaxFrameSize = maxSize;
+    }
+
+    //----- Accepts encoded AMQP frames for processing
+
+    @Override
+    public void accept(ByteBuffer buffer) {
+        accept(Unpooled.wrappedBuffer(buffer));
+    }
+
+    public void accept(ByteBuf buffer) {
+        LOG.trace("{} processing new inbound buffer of size: {}", driverName, buffer.readableBytes());
+
+        try {
+            // Process off all encoded frames from this buffer one at a time.
+            while (buffer.isReadable() && failureCause == null) {
+                LOG.trace("{} ingesting {} bytes.", driverName, buffer.readableBytes());
+                frameParser.ingest(buffer);
+                LOG.trace("{} ingestion completed cycle, remaining bytes in buffer: {}", driverName, buffer.readableBytes());
+            }
+        } catch (AssertionError e) {
+            signalFailure(e);
+        }
+    }
+
+    //----- Test driver handling of decoded AMQP frames
+
+    void handleHeader(AMQPHeader header) throws AssertionError {
+        synchronized (script) {
+            final ScriptedElement scriptEntry = script.poll();
+
+            if (scriptEntry == null) {
+                signalFailure(new AssertionError("Received header when not expecting any input."));
+            }
+
+            try {
+                header.invoke(scriptEntry, this);
+            } catch (Throwable t) {
+                if (scriptEntry.isOptional()) {
+                    handleHeader(header);
+                } else {
+                    LOG.warn(t.getMessage());
+                    signalFailure(t);
+                    throw t;
+                }
+            }
+
+            prcessScript(scriptEntry);
+        }
+    }
+
+    void handleSaslPerformative(SaslDescribedType sasl, int channel, ByteBuf payload) throws AssertionError {
+        synchronized (script) {
+            final ScriptedElement scriptEntry = script.poll();
+            if (scriptEntry == null) {
+                signalFailure(new AssertionError("Received performative[" + sasl + "] when not expecting any input."));
+            }
+
+            try {
+                sasl.invoke(scriptEntry, this);
+            } catch (UnexpectedPerformativeError e) {
+                if (scriptEntry.isOptional()) {
+                    handleSaslPerformative(sasl, channel, payload);
+                } else {
+                    signalFailure(e);
+                    throw e;
+                }
+            } catch (AssertionError assertion) {
+                LOG.warn(assertion.getMessage());
+                signalFailure(assertion);
+                throw assertion;
+            }
+
+            prcessScript(scriptEntry);
+        }
+    }
+
+    void handlePerformative(PerformativeDescribedType amqp, int channel, ByteBuf payload) throws AssertionError {
+        if (!amqp.getPerformativeType().equals(PerformativeType.HEARTBEAT)) {
+            performativeCount++;
+        }
+
+        synchronized (script) {
+            final ScriptedElement scriptEntry = script.poll();
+            if (scriptEntry == null) {
+                // TODO - Need to ensure a readable error by converting the codec type to a true performative type when
+                //        logging what happened here.
+                signalFailure(new AssertionError("Received performative[" + amqp + "] when not expecting any input."));
+            }
+
+            try {
+                amqp.invoke(scriptEntry, payload, channel, this);
+            } catch (UnexpectedPerformativeError e) {
+                if (scriptEntry.isOptional()) {
+                    handlePerformative(amqp, channel, payload);
+                } else {
+                    signalFailure(e);
+                    throw e;
+                }
+            } catch (AssertionError assertion) {
+                LOG.warn(assertion.getMessage());
+                signalFailure(assertion);
+                throw assertion;
+            }
+
+            prcessScript(scriptEntry);
+        }
+    }
+
+    void handleHeartbeat(int channel) {
+        emptyFrameCount++;
+        handlePerformative(HeartBeat.INSTANCE, channel, null);
+    }
+
+    public synchronized void afterDelay(int delay, ScriptedAction action) {
+        Objects.requireNonNull(schedulerSupplier, "This driver cannot schedule delayed events, no scheduler available");
+        ScheduledExecutorService scheduler = schedulerSupplier.get();
+        Objects.requireNonNull(scheduler, "This driver cannot schedule delayed events, no scheduler available");
+
+        scheduler.schedule(() -> {
+            LOG.trace("{} running delayed action: {}", driverName, action);
+            action.perform(this);
+        }, delay, TimeUnit.MILLISECONDS);
+    }
+
+    //----- Test driver actions
+
+    public void waitForScriptToComplete() {
+        checkFailed();
+
+        ScriptCompleteAction possibleWait = null;
+
+        synchronized (script) {
+            checkFailed();
+            if (!script.isEmpty()) {
+                possibleWait = new ScriptCompleteAction(this).queue();
+            }
+        }
+
+        if (possibleWait != null) {
+            try {
+                possibleWait.await();
+            } catch (InterruptedException e) {
+                Thread.interrupted();
+                signalFailure("Interrupted while waiting for script to complete");
+            }
+        }
+
+        checkFailed();
+    }
+
+    public void waitForScriptToCompleteIgnoreErrors() {
+        ScriptCompleteAction possibleWait = null;
+
+        synchronized (script) {
+            if (!script.isEmpty()) {
+                possibleWait = new ScriptCompleteAction(this).queue();
+            }
+        }
+
+        if (possibleWait != null) {
+            try {
+                possibleWait.await();
+            } catch (InterruptedException e) {
+                Thread.interrupted();
+                signalFailure("Interrupted while waiting for script to complete");
+            }
+        }
+    }
+
+    public void waitForScriptToComplete(long timeout) {
+        waitForScriptToComplete(timeout, TimeUnit.SECONDS);
+    }
+
+    public void waitForScriptToComplete(long timeout, TimeUnit units) {
+        checkFailed();
+
+        ScriptCompleteAction possibleWait = null;
+
+        synchronized (script) {
+            checkFailed();
+            if (!script.isEmpty()) {
+                possibleWait = new ScriptCompleteAction(this).queue();
+            }
+        }
+
+        if (possibleWait != null) {
+            try {
+                possibleWait.await(timeout, units);
+            } catch (InterruptedException e) {
+                Thread.interrupted();
+                signalFailure("Interrupted while waiting for script to complete");
+            }
+        }
+
+        checkFailed();
+    }
+
+    public void addScriptedElement(ScriptedElement element) {
+        checkFailed();
+        synchronized (script) {
+            checkFailed();
+            script.offer(element);
+        }
+    }
+
+    /**
+     * Encodes the given frame data into a ProtonBuffer and injects it into the configured consumer.
+     *
+     * @param channel
+     *      The channel to use when writing the frame
+     * @param performative
+     *      The AMQP Performative to write
+     * @param payload
+     *      The payload to include in the encoded frame.
+     */
+    public void sendAMQPFrame(int channel, DescribedType performative, ByteBuf payload) {
+        LOG.trace("{} Sending performative: {}", driverName, performative);
+        // TODO - handle split frames when frame size requires it
+
+        try {
+            final ByteBuf buffer = frameEncoder.handleWrite(performative, channel, payload, null);
+            LOG.trace("{} Writing out buffer {} to consumer: {}", driverName, buffer, frameConsumer);
+            frameConsumer.accept(buffer.nioBuffer());
+        } catch (Throwable t) {
+            signalFailure(new AssertionError("Frame was not written due to error.", t));
+        }
+    }
+
+    /**
+     * Encodes the given frame data into a ProtonBuffer and injects it into the configured consumer.
+     *
+     * @param channel
+     *      The channel to use when writing the frame
+     * @param performative
+     *      The SASL Performative to write
+     */
+    public void sendSaslFrame(int channel, DescribedType performative) {
+        // When the outcome of SASL is written the decoder should revert to initial state
+        // as the only valid next incoming value is an AMQP header.
+        if (performative instanceof SaslOutcome) {
+            frameParser.resetToExpectingHeader();
+        }
+
+        LOG.trace("{} Sending sasl performative: {}", driverName, performative);
+
+        try {
+            final ByteBuf buffer = frameEncoder.handleWrite(performative, channel);
+            frameConsumer.accept(buffer.nioBuffer());
+        } catch (Throwable t) {
+            signalFailure(new AssertionError("Frame was not written due to error.", t));
+        }
+    }
+
+    /**
+     * Send the specific header bytes to the remote frame consumer.
+
+     * @param header
+     *      The byte array to send as the AMQP Header.
+     */
+    public void sendHeader(AMQPHeader header) {
+        LOG.trace("{} Sending AMQP Header: {}", driverName, header);
+        try {
+            frameConsumer.accept(ByteBuffer.wrap(header.getBuffer()));
+        } catch (Throwable t) {
+            signalFailure(new AssertionError("Frame was not consumed due to error.", t));
+        }
+    }
+
+    /**
+     * Send an Empty Frame on the given channel to the remote consumer.
+     *
+     * @param channel
+     *      the channel on which to send the empty frame.
+     */
+    public void sendEmptyFrame(int channel) {
+        ByteBuf buffer = frameEncoder.handleWrite(null, channel, null, null);
+
+        try {
+            frameConsumer.accept(buffer.nioBuffer());
+        } catch (Throwable t) {
+            signalFailure(new AssertionError("Frame was not consumed due to error.", t));
+        }
+    }
+
+    /**
+     * Send the specific ProtonBuffer bytes to the remote frame consumer.
+
+     * @param buffer
+     *      The buffer whose contents are to be written to the frame consumer.
+     */
+    public void sendBytes(ByteBuffer buffer) {
+        LOG.trace("{} Sending bytes from ByteBuffer: {}", driverName, buffer);
+        try {
+            frameConsumer.accept(buffer.duplicate());
+        } catch (Throwable t) {
+            signalFailure(new AssertionError("Buffer was not consumed due to error.", t));
+        }
+    }
+
+    /**
+     * Send the specific ProtonBuffer bytes to the remote frame consumer.
+
+     * @param buffer
+     *      The buffer whose contents are to be written to the frame consumer.
+     */
+    public void sendBytes(ByteBuf buffer) {
+        LOG.trace("{} Sending bytes from ProtonBuffer: {}", driverName, buffer);
+        try {
+            frameConsumer.accept(buffer.nioBuffer());
+        } catch (Throwable t) {
+            signalFailure(new AssertionError("Buffer was not consumed due to error.", t));
+        }
+    }
+
+    /**
+     * Throw an exception from processing incoming data which should be handled by the peer under test.
+     *
+     * @param ex
+     *      The exception that triggered this call.
+     *
+     * @throws AssertionError indicating the first error that cause the driver to report test failure.
+     */
+    public void signalFailure(Throwable ex) throws AssertionError {
+        if (this.failureCause == null) {
+            if (ex instanceof AssertionError) {
+                LOG.trace("{} sending failure assertion due to: ", driverName, ex);
+                this.failureCause = (AssertionError) ex;
+            } else {
+                LOG.trace("{} sending failure assertion due to: ", driverName, ex);
+                this.failureCause = new AssertionError(ex);
+            }
+
+            searchForScriptioCompletionAndTrigger();
+
+            if (assertionConsumer != null) {
+                assertionConsumer.accept(failureCause);
+            }
+        }
+    }
+
+    /**
+     * Throw an exception from processing incoming data which should be handled by the peer under test.
+     *
+     * @param message
+     *      The error message that describes what triggered this call.
+     *
+     * @throws AssertionError that indicates the first error that failed for this driver.
+     */
+    public void signalFailure(String message) throws AssertionError {
+        signalFailure(new AssertionError(message));
+    }
+
+    //----- Internal implementation
+
+    private void searchForScriptioCompletionAndTrigger() {
+        script.forEach(element -> {
+            if (element instanceof ScriptCompleteAction) {
+                ScriptCompleteAction completed = (ScriptCompleteAction) element;
+                completed.perform(this);
+            }
+        });
+    }
+
+    private void prcessScript(ScriptedElement current) {
+        while (current.performAfterwards() != null && failureCause == null) {
+            current.performAfterwards().perform(this);
+        }
+
+        ScriptedElement peekNext = script.peek();
+        do {
+            if (peekNext instanceof ScriptedAction) {
+                script.poll();
+                ((ScriptedAction) peekNext).perform(this);
+            } else {
+                return;
+            }
+
+            peekNext = script.peek();
+        } while (peekNext != null && failureCause == null);
+    }
+
+    private void checkFailed() {
+        if (failureCause != null) {
+            throw failureCause;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/DriverSessions.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/DriverSessions.java
new file mode 100644
index 0000000..2ac4935
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/DriverSessions.java
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+
+/**
+ * Tracks all sessions opened by the remote or initiated from the driver.
+ */
+public class DriverSessions {
+
+    public static final int DRIVER_DEFAULT_CHANNEL_MAX = 65535;
+
+    private final Map<UnsignedShort, SessionTracker> localSessions = new LinkedHashMap<>();
+    private final Map<UnsignedShort, SessionTracker> remoteSessions = new LinkedHashMap<>();
+
+    private final AMQPTestDriver driver;
+
+    private UnsignedShort lastRemotelyOpenedSession = null;
+    private UnsignedShort lastLocallyOpenedSession = null;
+    private LinkTracker lastCoordinator;
+
+    public DriverSessions(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    public SessionTracker getLastRemotelyOpenedSession() {
+        return localSessions.get(lastRemotelyOpenedSession);
+    }
+
+    public SessionTracker getLastLocallyOpenedSession() {
+        return localSessions.get(lastLocallyOpenedSession);
+    }
+
+    public LinkTracker getLastOpenedCoordinator() {
+        return lastCoordinator;
+    }
+
+    void setLastOpenedCoordinator(LinkTracker lastOpenedCoordinatorLink) {
+        this.lastCoordinator = lastOpenedCoordinatorLink;
+    }
+
+    public AMQPTestDriver getDriver() {
+        return driver;
+    }
+
+    public SessionTracker getSessionFromLocalChannel(UnsignedShort localChannel) {
+        return localSessions.get(localChannel);
+    }
+
+    public SessionTracker getSessionFromRemoteChannel(UnsignedShort remoteChannel) {
+        return remoteSessions.get(remoteChannel);
+    }
+
+    //----- Process performatives that require session level tracking
+
+    public SessionTracker handleBegin(Begin remoteBegin, UnsignedShort remoteChannel) {
+        if (remoteSessions.containsKey(remoteChannel)) {
+            throw new AssertionError("Received duplicate Begin for already opened session on channel: " + remoteChannel);
+        }
+
+        final SessionTracker sessionTracker;  // Result that we need to update here once validation is complete.
+
+        if (remoteBegin.getRemoteChannel() != null) {
+            // This should be a response to previous Begin that this test driver sent if there
+            // is a remote channel set in which case a local session should already have been
+            // created and if not that is an error
+            sessionTracker = localSessions.get(remoteBegin.getRemoteChannel());
+            if (sessionTracker == null) {
+                throw new AssertionError(String.format(
+                    "Received Begin on channel [%d] that indicated it was a response to a Begin this driver never sent to channel [%d]: ",
+                    remoteChannel, remoteBegin.getRemoteChannel()));
+            }
+        } else {
+            // Remote has requested that the driver create a new session which will require a scripted
+            // response in order to complete the begin cycle.  Start tracking now for future
+            sessionTracker = new SessionTracker(driver);
+
+            localSessions.put(sessionTracker.getLocalChannel(), sessionTracker);
+        }
+
+        sessionTracker.handleBegin(remoteBegin, remoteChannel);
+
+        remoteSessions.put(remoteChannel, sessionTracker);
+        lastRemotelyOpenedSession = sessionTracker.getLocalChannel();
+
+        return sessionTracker;
+    }
+
+    public SessionTracker handleEnd(End remoteEnd, UnsignedShort remoteChannel) {
+        SessionTracker sessionTracker = remoteSessions.get(remoteChannel);
+
+        if (sessionTracker == null) {
+            throw new AssertionError(String.format(
+                "Received End on channel [%d] that has no matching Session for that remote channel. ", remoteChannel));
+        } else {
+            sessionTracker.handleEnd(remoteEnd);
+            remoteSessions.remove(remoteChannel);
+
+            return sessionTracker;
+        }
+    }
+
+    //----- Process Session Begin and End from their injection actions and update state
+
+    public SessionTracker handleLocalBegin(Begin localBegin, UnsignedShort localChannel) {
+        // Are we responding to a remote Begin?  If so then we already have a SessionTracker
+        // that should be correlated with the local tracker stored now that we are responding
+        // to, although a test might be fiddling with unexpected Begin commands so we don't
+        // assume there absolutely must be a remote session in the tracking map.
+        if (localBegin.getRemoteChannel() != null && remoteSessions.containsKey(localBegin.getRemoteChannel())) {
+            localSessions.put(localChannel, remoteSessions.get(localBegin.getRemoteChannel()));
+        }
+
+        if (!localSessions.containsKey(localChannel)) {
+            localSessions.put(localChannel, new SessionTracker(driver));
+        }
+
+        lastLocallyOpenedSession = localChannel;
+
+        return localSessions.get(localChannel).handleLocalBegin(localBegin, localChannel);
+    }
+
+    public SessionTracker handleLocalEnd(End localEnd, UnsignedShort localChannel) {
+        // A test script might trigger multiple end calls or otherwise mess with normal
+        // AMQP processing no in case we can't find it, just return a dummy that the
+        // script can use.
+        if (localSessions.containsKey(localChannel)) {
+            return localSessions.get(localChannel).handleLocalEnd(localEnd);
+        } else {
+            return new SessionTracker(driver).handleLocalEnd(localEnd);
+        }
+    }
+
+    //----- Driver Session Management API
+
+    public int findFreeLocalChannel() {
+        // TODO: Respect local channel max if one was set on open.
+        for (int i = 0; i <= DRIVER_DEFAULT_CHANNEL_MAX; ++i) {
+            if (!localSessions.containsKey(UnsignedShort.valueOf(i))) {
+                return i;
+            }
+        }
+
+        throw new IllegalStateException("no local channel available for allocation");
+    }
+
+    void freeLocalChannel(UnsignedShort localChannel) {
+        localSessions.remove(localChannel);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/FrameDecoder.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/FrameDecoder.java
new file mode 100644
index 0000000..02fd1b9
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/FrameDecoder.java
@@ -0,0 +1,357 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import org.apache.qpid.protonj2.test.driver.codec.Codec;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.codec.transport.HeartBeat;
+import org.apache.qpid.protonj2.test.driver.codec.transport.PerformativeDescribedType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+class FrameDecoder {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AMQPTestDriver.class);
+
+    public static final byte AMQP_FRAME_TYPE = (byte) 0;
+    public static final byte SASL_FRAME_TYPE = (byte) 1;
+
+    public static final int FRAME_SIZE_BYTES = 4;
+
+    private final AMQPTestDriver driver;
+    private final Codec codec = Codec.Factory.create();
+    private FrameParserStage stage = new HeaderParsingStage();
+
+    // Parser stages used during the parsing process
+    private final FrameSizeParsingStage frameSizeParser = new FrameSizeParsingStage();
+    private final FrameBufferingStage frameBufferingStage = new FrameBufferingStage();
+    private final FrameParserStage frameBodyParsingStage = new FrameBodyParsingStage();
+
+    public FrameDecoder(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    public void ingest(ByteBuf buffer) throws AssertionError {
+        try {
+            // Parses in-incoming data and emit one complete frame before returning, caller should
+            // ensure that the input buffer is drained into the engine or stop if the engine
+            // has changed to a non-writable state.
+            stage.parse(buffer);
+        } catch (AssertionError ex) {
+            transitionToErrorStage(ex);
+            throw ex;
+        } catch (Throwable throwable) {
+            AssertionError error = new AssertionError("Frame decode failed.", throwable);
+            transitionToErrorStage(error);
+            throw error;
+        }
+    }
+
+    /**
+     * Resets the parser back to the expect a header state.
+     */
+    public void resetToExpectingHeader() {
+        this.stage = new HeaderParsingStage();
+    }
+
+    //---- Methods to transition between stages
+
+    private FrameParserStage transitionToFrameSizeParsingStage() {
+        return stage = frameSizeParser.reset(0);
+    }
+
+    private FrameParserStage transitionToFrameBufferingStage(int frameSize) {
+        return stage = frameBufferingStage.reset(frameSize);
+    }
+
+    private FrameParserStage initializeFrameBodyParsingStage(int frameSize) {
+        return stage = frameBodyParsingStage.reset(frameSize);
+    }
+
+    private ParsingErrorStage transitionToErrorStage(AssertionError error) {
+        if (!(stage instanceof ParsingErrorStage)) {
+            stage = new ParsingErrorStage(error);
+        }
+
+        return (ParsingErrorStage) stage;
+    }
+
+    //----- Frame Parsing Stage definition
+
+    private interface FrameParserStage {
+
+        /**
+         * Parse the incoming data and provide events to the parent Transport
+         * based on the contents of that data.
+         *
+         * @param input
+         *      The ByteBuf containing new data to be parsed.
+         *
+         * @throws AssertionError if an error occurs while parsing incoming data.
+         */
+        void parse(ByteBuf input) throws AssertionError;
+
+        /**
+         * Reset the stage to its defaults for a new cycle of parsing.
+         *
+         * @param frameSize
+         *      The frameSize to use for this part of the parsing operation
+         *
+         * @return a reference to this parsing stage for chaining.
+         */
+        FrameParserStage reset(int frameSize);
+
+    }
+
+    //---- Built in FrameParserStages
+
+    private class HeaderParsingStage implements FrameParserStage {
+
+        private final byte[] headerBytes = new byte[AMQPHeader.HEADER_SIZE_BYTES];
+
+        private int headerByte;
+
+        @Override
+        public void parse(ByteBuf incoming) throws AssertionError {
+            while (incoming.isReadable() && headerByte < AMQPHeader.HEADER_SIZE_BYTES) {
+                headerBytes[headerByte++] = incoming.readByte();
+            }
+
+            if (headerByte == AMQPHeader.HEADER_SIZE_BYTES) {
+                // Construct a new Header from the read bytes which will validate the contents
+                AMQPHeader header = new AMQPHeader(headerBytes);
+
+                // Transition to parsing the frames if any pipelined into this buffer.
+                transitionToFrameSizeParsingStage();
+
+                if (header.isSaslHeader()) {
+                    driver.handleHeader(AMQPHeader.getSASLHeader());
+                } else {
+                    driver.handleHeader(AMQPHeader.getAMQPHeader());
+                }
+            }
+        }
+
+        @Override
+        public HeaderParsingStage reset(int frameSize) {
+            headerByte = 0;
+            return this;
+        }
+    }
+
+    private class FrameSizeParsingStage implements FrameParserStage {
+
+        private int frameSize;
+        private int multiplier = FRAME_SIZE_BYTES;
+
+        @Override
+        public void parse(ByteBuf input) throws AssertionError {
+            while (input.isReadable()) {
+                frameSize |= ((input.readByte() & 0xFF) << --multiplier * Byte.SIZE);
+                if (multiplier == 0) {
+                    break;
+                }
+            }
+
+            if (multiplier == 0) {
+                validateFrameSize();
+
+                // Normalize the frame size to the reminder portion
+                int length = frameSize - FRAME_SIZE_BYTES;
+
+                if (input.readableBytes() < length) {
+                    transitionToFrameBufferingStage(length);
+                } else {
+                    initializeFrameBodyParsingStage(length);
+                }
+
+                stage.parse(input);
+            }
+        }
+
+        private void validateFrameSize() throws AssertionError {
+            if (frameSize < 8) {
+               throw new AssertionError(String.format(
+                    "specified frame size %d smaller than minimum frame header size 8", frameSize));
+            }
+
+            if (frameSize > driver.getInboundMaxFrameSize()) {
+                throw new AssertionError(String.format(
+                    "specified frame size %d larger than maximum frame size %d", frameSize, driver.getInboundMaxFrameSize()));
+            }
+        }
+
+        @Override
+        public FrameSizeParsingStage reset(int frameSize) {
+            multiplier = FRAME_SIZE_BYTES;
+            this.frameSize = frameSize;
+            return this;
+        }
+    }
+
+    private class FrameBufferingStage implements FrameParserStage {
+
+        private ByteBuf buffer;
+
+        @Override
+        public void parse(ByteBuf input) throws AssertionError {
+            if (input.readableBytes() < buffer.writableBytes()) {
+                buffer.writeBytes(input);
+            } else {
+                buffer.writeBytes(input, buffer.writableBytes());
+
+                // Now we can consume the buffer frame body.
+                initializeFrameBodyParsingStage(buffer.readableBytes());
+                try {
+                    stage.parse(buffer);
+                } finally {
+                    buffer = null;
+                }
+            }
+        }
+
+        @Override
+        public FrameBufferingStage reset(int frameSize) {
+            buffer = Unpooled.buffer(frameSize, frameSize);
+            return this;
+        }
+    }
+
+    private class FrameBodyParsingStage implements FrameParserStage {
+
+        private int frameSize;
+
+        @Override
+        public void parse(ByteBuf input) throws AssertionError {
+            int dataOffset = (input.readByte() << 2) & 0x3FF;
+            int frameSize = this.frameSize + FRAME_SIZE_BYTES;
+
+            validateDataOffset(dataOffset, frameSize);
+
+            int type = input.readByte() & 0xFF;
+            short channel = input.readShort();
+
+            // note that this skips over the extended header if it's present
+            if (dataOffset != 8) {
+                input.readerIndex(input.readerIndex() + dataOffset - 8);
+            }
+
+            final int frameBodySize = frameSize - dataOffset;
+
+            ByteBuf payload = null;
+            Object val = null;
+
+            if (frameBodySize > 0) {
+                int frameBodyStartIndex = input.readerIndex();
+
+                try {
+                    codec.decode(input);
+                } catch (Exception e) {
+                    throw new AssertionError("Decoder failed reading remote input:", e);
+                }
+
+                Codec.DataType dataType = codec.type();
+                if (dataType != Codec.DataType.DESCRIBED) {
+                    throw new IllegalArgumentException(
+                        "Frame body type expected to be " + Codec.DataType.DESCRIBED + " but was: " + dataType);
+                }
+
+                try {
+                    val = codec.getDescribedType();
+                } finally {
+                    codec.clear();
+                }
+
+                // Slice to the known Frame body size and use that as the buffer for any payload once
+                // the actual Performative has been decoded.  The implies that the data comprising the
+                // performative will be held as long as the payload buffer is kept.
+                if (input.isReadable()) {
+                    // Check that the remaining bytes aren't part of another frame.
+                    int payloadSize = frameBodySize - (input.readerIndex() - frameBodyStartIndex);
+                    if (payloadSize > 0) {
+                        payload = input.slice(input.readerIndex(), payloadSize);
+                        input.skipBytes(payloadSize);
+                    }
+                }
+            } else {
+                LOG.trace("{} Read: CH[{}] : {} [{}]", driver.getName(), channel, HeartBeat.INSTANCE, payload);
+                transitionToFrameSizeParsingStage();
+                driver.handleHeartbeat(channel);
+                return;
+            }
+
+            if (type == AMQP_FRAME_TYPE) {
+                PerformativeDescribedType performative = (PerformativeDescribedType) val;
+                LOG.trace("{} Read: CH[{}] : {} [{}]", driver.getName(), channel, performative, payload);
+                transitionToFrameSizeParsingStage();
+                driver.handlePerformative(performative, channel, payload);
+            } else if (type == SASL_FRAME_TYPE) {
+                SaslDescribedType performative = (SaslDescribedType) val;
+                LOG.trace("{} Read: {} [{}]", driver.getName(), performative, payload);
+                transitionToFrameSizeParsingStage();
+                driver.handleSaslPerformative(performative, channel, payload);
+            } else {
+                throw new AssertionError(String.format("unknown frame type: %d", type));
+            }
+        }
+
+        @Override
+        public FrameBodyParsingStage reset(int frameSize) {
+            this.frameSize = frameSize;
+            return this;
+        }
+
+        private void validateDataOffset(int dataOffset, int frameSize) {
+            if (dataOffset < 8) {
+                throw new AssertionError(String.format(
+                    "specified frame data offset %d smaller than minimum frame header size %d", dataOffset, 8));
+            }
+
+            if (dataOffset > frameSize) {
+                throw new AssertionError(String.format(
+                    "specified frame data offset %d larger than the frame size %d", dataOffset, frameSize));
+            }
+        }
+    }
+
+    /*
+     * If parsing fails the parser enters the failed state and remains there always throwing the given exception
+     * if additional parsing is requested.
+     */
+    private class ParsingErrorStage implements FrameParserStage {
+
+        private final AssertionError parsingError;
+
+        public ParsingErrorStage(AssertionError parsingError) {
+            this.parsingError = parsingError;
+        }
+
+        @Override
+        public void parse(ByteBuf input) throws AssertionError {
+            throw parsingError;
+        }
+
+        @Override
+        public ParsingErrorStage reset(int frameSize) {
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/FrameEncoder.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/FrameEncoder.java
new file mode 100644
index 0000000..eecba3b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/FrameEncoder.java
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import org.apache.qpid.protonj2.test.driver.codec.Codec;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Encodes AMQP performatives into frames for transmission
+ */
+public class FrameEncoder {
+
+    public static final byte AMQP_FRAME_TYPE = (byte) 0;
+    public static final byte SASL_FRAME_TYPE = (byte) 1;
+
+    private static final int AMQP_PERFORMATIVE_PAD = 512;
+    private static final int FRAME_HEADER_SIZE = 8;
+
+    private static final int FRAME_START_BYTE = 0;
+    private static final int FRAME_DOFF_BYTE = 4;
+    private static final int FRAME_DOFF_SIZE = 2;
+    private static final int FRAME_TYPE_BYTE = 5;
+    private static final int FRAME_CHANNEL_BYTE = 6;
+
+    private final AMQPTestDriver driver;
+
+    private final Codec codec = Codec.Factory.create();
+
+    public FrameEncoder(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    public ByteBuf handleWrite(DescribedType performative, int channel, ByteBuf payload, Runnable payloadToLarge) {
+        return writeFrame(performative, payload, AMQP_FRAME_TYPE, channel, driver.getOutboundMaxFrameSize(), payloadToLarge);
+    }
+
+    public ByteBuf handleWrite(DescribedType performative, int channel) {
+        return writeFrame(performative, null, SASL_FRAME_TYPE, (short) 0, driver.getOutboundMaxFrameSize(), null);
+    }
+
+    private ByteBuf writeFrame(DescribedType performative, ByteBuf payload, byte frameType, int channel, int maxFrameSize, Runnable onPayloadTooLarge) {
+        int outputBufferSize = AMQP_PERFORMATIVE_PAD + (payload != null ? payload.readableBytes() : 0);
+
+        ByteBuf output = Unpooled.buffer(outputBufferSize);
+
+        final int performativeSize = writePerformative(performative, payload, maxFrameSize, output, onPayloadTooLarge);
+        final int capacity = maxFrameSize > 0 ? maxFrameSize - performativeSize : Integer.MAX_VALUE;
+        final int payloadSize = Math.min(payload == null ? 0 : payload.readableBytes(), capacity);
+
+        if (payloadSize > 0) {
+            output.writeBytes(payload, payloadSize);
+        }
+
+        endFrame(output, frameType, channel);
+
+        return output;
+    }
+
+    private int writePerformative(DescribedType performative, ByteBuf payload, int maxFrameSize, ByteBuf output, Runnable onPayloadTooLarge) {
+        output.writerIndex(FRAME_HEADER_SIZE);
+
+        long encodedSize = 0;
+        int startIndex = output.writerIndex();
+
+        if (performative != null) {
+            try {
+                codec.putDescribedType(performative);
+                encodedSize = codec.encode(output);
+            } finally {
+                codec.clear();
+            }
+        }
+
+        int performativeSize = output.writerIndex() - startIndex;
+
+        if (performativeSize != encodedSize) {
+            throw new IllegalStateException(String.format(
+                "Unable to encode performative %s of %d bytes into provided proton buffer, only wrote %d bytes",
+                performative, performativeSize, encodedSize));
+        }
+
+        if (onPayloadTooLarge != null && maxFrameSize > 0 && payload != null && (payload.readableBytes() + performativeSize) > maxFrameSize) {
+            // Next iteration will re-encode the frame body again with updates from the <payload-to-large>
+            // handler and then we can move onto the body portion.
+            onPayloadTooLarge.run();
+            performativeSize = writePerformative(performative, payload, maxFrameSize, output, null);
+        }
+
+        return performativeSize;
+    }
+
+    private static void endFrame(ByteBuf output, byte frameType, int channel) {
+        output.setInt(FRAME_START_BYTE, output.readableBytes());
+        output.setByte(FRAME_DOFF_BYTE, FRAME_DOFF_SIZE);
+        output.setByte(FRAME_TYPE_BYTE, frameType);
+        output.setShort(FRAME_CHANNEL_BYTE, (short) channel);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/LinkTracker.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/LinkTracker.java
new file mode 100644
index 0000000..cafb9ca
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/LinkTracker.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.SenderSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Tracks information about links that are opened be the client under test.
+ */
+public abstract class LinkTracker {
+
+    private final SessionTracker session;
+    private final Attach attach;
+
+    public LinkTracker(SessionTracker session, Attach attach) {
+        this.session = session;
+        this.attach = attach;
+    }
+
+    public SessionTracker getSession() {
+        return session;
+    }
+
+    public String getName() {
+        return attach.getName();
+    }
+
+    public Role getRole() {
+        return isSender() ? Role.SENDER : Role.RECEIVER;
+    }
+
+    public SenderSettleMode getSenderSettleMode() {
+        return attach.getSenderSettleMode() != null ? SenderSettleMode.valueOf(attach.getSenderSettleMode()) : SenderSettleMode.MIXED;
+    }
+
+    public ReceiverSettleMode getReceiverSettleMode() {
+        return attach.getReceiverSettleMode() != null ? ReceiverSettleMode.valueOf(attach.getReceiverSettleMode()) : ReceiverSettleMode.FIRST;
+    }
+
+    public UnsignedInteger getHandle() {
+        return attach.getHandle();
+    }
+
+    public Source getSource() {
+        return attach.getSource();
+    }
+
+    public Target getTarget() {
+        return attach.getTarget() instanceof Target ? (Target) attach.getTarget() : null;
+    }
+
+    public Coordinator getCoordinator() {
+        return attach.getTarget() instanceof Coordinator ? (Coordinator) attach.getTarget() : null;
+    }
+
+    public boolean isSender() {
+        return Role.RECEIVER.getValue() == attach.getRole();
+    }
+
+    public boolean isReceiver() {
+        return Role.SENDER.getValue() == attach.getRole();
+    }
+
+    protected abstract void handleTransfer(Transfer transfer, ByteBuf payload);
+
+    protected abstract void handleFlow(Flow flow);
+
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestClient.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestClient.java
new file mode 100644
index 0000000..9541f4b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestClient.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.netty.NettyClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.EventLoop;
+import io.netty.channel.SimpleChannelInboundHandler;
+
+/**
+ * Test Client for AMQP server testing, allows for scripting the expected inputs from
+ * the server and outputs from the client back to the server.
+ */
+public class ProtonTestClient extends ProtonTestPeer implements AutoCloseable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ProtonTestClient.class);
+
+    private final AMQPTestDriver driver;
+    private final NettyTestDriverClient client;
+
+    /**
+     * Creates a Socket Test Peer using all default Server options.
+     */
+    public ProtonTestClient() {
+        this(new ProtonTestClientOptions());
+    }
+
+    @Override
+    public String getPeerName() {
+        return "Client";
+    }
+
+    /**
+     * Creates a Test Client using the options to configure the client connection.
+     *
+     * @param options
+     *      The options that control the behavior of the client connection.
+     */
+    public ProtonTestClient(ProtonTestClientOptions options) {
+        this.driver = new NettyAwareAMQPTestDriver(this::processDriverOutput, this::processDriverAssertion, this::eventLoop);
+        this.client = new NettyTestDriverClient(options);
+    }
+
+    public void connect(String hostname, int port) throws IOException {
+        client.connect(hostname, port);
+    }
+
+    @Override
+    public AMQPTestDriver getDriver() {
+        return driver;
+    }
+
+    @Override
+    protected void processCloseRequest() {
+        try {
+            client.close();
+        } catch (Throwable e) {
+            LOG.info("Error suppressed on client stop: ", e);
+        }
+    }
+
+    @Override
+    protected void processDriverOutput(ByteBuffer frame) {
+        LOG.trace("AMQP Server Channel writing: {}", frame);
+        client.write(frame);
+    }
+
+    protected void processChannelInput(ByteBuf input) {
+        LOG.trace("AMQP Test Client Channel processing: {}", input);
+        driver.accept(input);
+    }
+
+    protected void processDriverAssertion(AssertionError error) {
+        LOG.trace("AMQP Test Client Closing due to error: {}", error.getMessage());
+        close();
+    }
+
+    protected ScheduledExecutorService eventLoop() {
+        return client.eventLoop();
+    }
+
+    //----- Test driver Wrapper to ensure actions occur on the event loop
+
+    private final class NettyAwareAMQPTestDriver extends AMQPTestDriver {
+
+        public NettyAwareAMQPTestDriver(Consumer<ByteBuffer> frameConsumer, Consumer<AssertionError> assertionConsumer, Supplier<ScheduledExecutorService> scheduler) {
+            super(getPeerName(), frameConsumer, assertionConsumer, scheduler);
+        }
+
+        // If the send call occurs from a reaction to processing incoming data the
+        // call will be on the event loop but for actions requested by the test that
+        // are directed to happen immediately they will be running on the test thread
+        // and so we direct the resulting action into the event loop to avoid codec or
+        // other driver resources being used on two different threads.
+
+        @Override
+        public void sendAMQPFrame(int channel, DescribedType performative, ByteBuf payload) {
+            EventLoop loop = client.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendAMQPFrame(channel, performative, payload);
+            } else {
+                loop.execute(() -> {
+                    super.sendAMQPFrame(channel, performative, payload);
+                });
+            }
+        }
+
+        @Override
+        public void sendSaslFrame(int channel, DescribedType performative) {
+            EventLoop loop = client.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendSaslFrame(channel, performative);
+            } else {
+                loop.execute(() -> {
+                    super.sendSaslFrame(channel, performative);
+                });
+            }
+        }
+
+        @Override
+        public void sendHeader(AMQPHeader header) {
+            EventLoop loop = client.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendHeader(header);
+            } else {
+                loop.execute(() -> {
+                    super.sendHeader(header);
+                });
+            }
+        }
+
+        @Override
+        public void sendEmptyFrame(int channel) {
+            EventLoop loop = client.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendEmptyFrame(channel);
+            } else {
+                loop.execute(() -> {
+                    super.sendEmptyFrame(channel);
+                });
+            }
+        }
+    }
+
+    //----- Channel handler that drives IO for the test driver
+
+    private final class NettyTestDriverClient extends NettyClient {
+
+        public NettyTestDriverClient(ProtonTestClientOptions options) {
+            super(options);
+        }
+
+        @Override
+        protected ChannelHandler getClientHandler() {
+            return new SimpleChannelInboundHandler<ByteBuf>() {
+
+                @Override
+                protected void channelRead0(ChannelHandlerContext ctx, ByteBuf input) throws Exception {
+                    LOG.trace("AMQP Test Client Channel read: {}", input);
+
+                    try {
+                        // Create a stable copy to avoid issue with retained buffer slices when input is pooled.
+                        ByteBuf copy = Unpooled.buffer(input.readableBytes());
+                        copy.writeBytes(input.nioBuffer());
+                        input.skipBytes(input.readableBytes());
+
+                        // Driver processes new data and may produce output based on this.
+                        processChannelInput(copy);
+                    } catch (Throwable e) {
+                        LOG.error("Closed AMQP Test client channel due to error: ", e);
+                        ctx.channel().close();
+                    }
+                }
+            };
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestClientOptions.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestClientOptions.java
new file mode 100644
index 0000000..c40cf20
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestClientOptions.java
@@ -0,0 +1,593 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLContext;
+
+/**
+ * Encapsulates all the Test Client options in one configuration object.
+ */
+public class ProtonTestClientOptions implements Cloneable {
+
+    public static final int DEFAULT_SEND_BUFFER_SIZE = 64 * 1024;
+    public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_SEND_BUFFER_SIZE;
+    public static final int DEFAULT_TRAFFIC_CLASS = 0;
+    public static final boolean DEFAULT_TCP_NO_DELAY = true;
+    public static final boolean DEFAULT_TCP_KEEP_ALIVE = false;
+    public static final int DEFAULT_SO_LINGER = Integer.MIN_VALUE;
+    public static final int DEFAULT_SO_TIMEOUT = -1;
+    public static final int DEFAULT_CONNECT_TIMEOUT = 60000;
+    public static final int DEFAULT_TCP_PORT = 5672;
+    public static final int DEFAULT_SSL_PORT = 5671;
+    public static final boolean DEFAULT_TRACE_BYTES = false;
+    public static final String DEFAULT_STORE_TYPE = "jks";
+    public static final String DEFAULT_CONTEXT_PROTOCOL = "TLS";
+    public static final boolean DEFAULT_TRUST_ALL = false;
+    public static final boolean DEFAULT_VERIFY_HOST = true;
+    public static final List<String> DEFAULT_DISABLED_PROTOCOLS = Collections.unmodifiableList(Arrays.asList(new String[]{"SSLv2Hello", "SSLv3"}));
+    public static final int DEFAULT_LOCAL_PORT = 0;
+    public static final boolean DEFAULT_USE_WEBSOCKETS = false;
+    public static final boolean DEFAULT_FRAGMENT_WEBSOCKET_WRITES = false;
+    public static final String DEFAULT_WEBSOCKET_PATH = "/";
+    public static final int DEFAULT_WEBSOCKET_MAX_FRAME_SIZE = 65535;
+    public static final boolean DEFAULT_SECURE_SERVER = false;
+    public static final boolean DEFAULT_NEEDS_CLIENT_AUTH = false;
+
+    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
+    private static final String JAVAX_NET_SSL_KEY_STORE_TYPE = "javax.net.ssl.keyStoreType";
+    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
+    private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword";
+
+    private int sendBufferSize = DEFAULT_SEND_BUFFER_SIZE;
+    private int receiveBufferSize = DEFAULT_RECEIVE_BUFFER_SIZE;
+    private int trafficClass = DEFAULT_TRAFFIC_CLASS;
+    private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;
+    private int soTimeout = DEFAULT_SO_TIMEOUT;
+    private int soLinger = DEFAULT_SO_LINGER;
+    private boolean tcpKeepAlive = DEFAULT_TCP_KEEP_ALIVE;
+    private boolean tcpNoDelay = DEFAULT_TCP_NO_DELAY;
+    private String localAddress;
+    private int localPort = DEFAULT_LOCAL_PORT;
+    private boolean traceBytes = DEFAULT_TRACE_BYTES;
+    private boolean useWebSockets = DEFAULT_USE_WEBSOCKETS;
+    private boolean fragmentWebSocketWrites = DEFAULT_FRAGMENT_WEBSOCKET_WRITES;
+    private String webSocketPath = DEFAULT_WEBSOCKET_PATH;
+    private int webSocketMaxFrameSize = DEFAULT_WEBSOCKET_MAX_FRAME_SIZE;
+
+    private boolean secure = DEFAULT_SECURE_SERVER;
+    private boolean needClientAuth = DEFAULT_NEEDS_CLIENT_AUTH;
+    private String keyStoreLocation;
+    private String keyStorePassword;
+    private String trustStoreLocation;
+    private String trustStorePassword;
+    private String keyStoreType;
+    private String trustStoreType;
+    private String[] enabledCipherSuites;
+    private String[] disabledCipherSuites;
+    private String[] enabledProtocols;
+    private String[] disabledProtocols = DEFAULT_DISABLED_PROTOCOLS.toArray(new String[0]);
+    private String contextProtocol = DEFAULT_CONTEXT_PROTOCOL;
+
+    private boolean trustAll = DEFAULT_TRUST_ALL;
+    private boolean verifyHost = DEFAULT_VERIFY_HOST;
+    private String keyAlias;
+    private SSLContext sslContextOverride;
+
+    private final Map<String, String> httpHeaders = new HashMap<>();
+
+    public ProtonTestClientOptions() {
+        setKeyStoreLocation(System.getProperty(JAVAX_NET_SSL_KEY_STORE));
+        setKeyStoreType(System.getProperty(JAVAX_NET_SSL_KEY_STORE_TYPE, DEFAULT_STORE_TYPE));
+        setKeyStorePassword(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD));
+        setTrustStoreLocation(System.getProperty(JAVAX_NET_SSL_TRUST_STORE));
+        setTrustStoreType(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_TYPE, DEFAULT_STORE_TYPE));
+        setTrustStorePassword(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD));
+    }
+
+    @Override
+    public ProtonTestClientOptions clone() {
+        return copyOptions(new ProtonTestClientOptions());
+    }
+
+    /**
+     * @return the currently set send buffer size in bytes.
+     */
+    public int getSendBufferSize() {
+        return sendBufferSize;
+    }
+
+    /**
+     * Sets the send buffer size in bytes, the value must be greater than zero
+     * or an {@link IllegalArgumentException} will be thrown.
+     *
+     * @param sendBufferSize
+     *        the new send buffer size for the TCP Transport.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public void setSendBufferSize(int sendBufferSize) {
+        if (sendBufferSize <= 0) {
+            throw new IllegalArgumentException("The send buffer size must be > 0");
+        }
+
+        this.sendBufferSize = sendBufferSize;
+    }
+
+    /**
+     * @return the currently configured receive buffer size in bytes.
+     */
+    public int getReceiveBufferSize() {
+        return receiveBufferSize;
+    }
+
+    /**
+     * Sets the receive buffer size in bytes, the value must be greater than zero
+     * or an {@link IllegalArgumentException} will be thrown.
+     *
+     * @param receiveBufferSize
+     *        the new receive buffer size for the TCP Transport.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public void setReceiveBufferSize(int receiveBufferSize) {
+        if (receiveBufferSize <= 0) {
+            throw new IllegalArgumentException("The send buffer size must be > 0");
+        }
+
+        this.receiveBufferSize = receiveBufferSize;
+    }
+
+    /**
+     * @return the currently configured traffic class value.
+     */
+    public int getTrafficClass() {
+        return trafficClass;
+    }
+
+    /**
+     * Sets the traffic class value used by the TCP connection, valid
+     * range is between 0 and 255.
+     *
+     * @param trafficClass
+     *        the new traffic class value.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public void setTrafficClass(int trafficClass) {
+        if (trafficClass < 0 || trafficClass > 255) {
+            throw new IllegalArgumentException("Traffic class must be in the range [0..255]");
+        }
+
+        this.trafficClass = trafficClass;
+    }
+
+    public int getSoTimeout() {
+        return soTimeout;
+    }
+
+    public void setSoTimeout(int soTimeout) {
+        this.soTimeout = soTimeout;
+    }
+
+    public int getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    public void setConnectTimeout(int connectTimeout) {
+        this.connectTimeout = connectTimeout;
+    }
+
+    public boolean isTcpNoDelay() {
+        return tcpNoDelay;
+    }
+
+    public void setTcpNoDelay(boolean tcpNoDelay) {
+        this.tcpNoDelay = tcpNoDelay;
+    }
+
+    public int getSoLinger() {
+        return soLinger;
+    }
+
+    public void setSoLinger(int soLinger) {
+        this.soLinger = soLinger;
+    }
+
+    public boolean isTcpKeepAlive() {
+        return tcpKeepAlive;
+    }
+
+    public void setTcpKeepAlive(boolean keepAlive) {
+        this.tcpKeepAlive = keepAlive;
+    }
+
+    public String getLocalAddress() {
+        return localAddress;
+    }
+
+    public void setLocalAddress(String localAddress) {
+        this.localAddress = localAddress;
+    }
+
+    public int getLocalPort() {
+        return localPort;
+    }
+
+    public void setLocalPort(int localPort) {
+        this.localPort = localPort;
+    }
+
+    /**
+     * @return true if the transport should enable byte tracing
+     */
+    public boolean isTraceBytes() {
+        return traceBytes;
+    }
+
+    /**
+     * Determines if the transport should add a logger for bytes in / out
+     *
+     * @param traceBytes
+     * 		should the transport log the bytes in and out.
+     */
+    public void setTraceBytes(boolean traceBytes) {
+        this.traceBytes = traceBytes;
+    }
+
+    /**
+     * @return the keyStoreLocation currently configured.
+     */
+    public String getKeyStoreLocation() {
+        return keyStoreLocation;
+    }
+
+    /**
+     * Sets the location on disk of the key store to use.
+     *
+     * @param keyStoreLocation
+     *        the keyStoreLocation to use to create the key manager.
+     */
+    public void setKeyStoreLocation(String keyStoreLocation) {
+        this.keyStoreLocation = keyStoreLocation;
+    }
+
+    /**
+     * @return the keyStorePassword
+     */
+    public String getKeyStorePassword() {
+        return keyStorePassword;
+    }
+
+    /**
+     * @param keyStorePassword the keyStorePassword to set
+     */
+    public void setKeyStorePassword(String keyStorePassword) {
+        this.keyStorePassword = keyStorePassword;
+    }
+
+    /**
+     * @return the trustStoreLocation
+     */
+    public String getTrustStoreLocation() {
+        return trustStoreLocation;
+    }
+
+    /**
+     * @param trustStoreLocation the trustStoreLocation to set
+     */
+    public void setTrustStoreLocation(String trustStoreLocation) {
+        this.trustStoreLocation = trustStoreLocation;
+    }
+
+    /**
+     * @return the trustStorePassword
+     */
+    public String getTrustStorePassword() {
+        return trustStorePassword;
+    }
+
+    /**
+     * @param trustStorePassword the trustStorePassword to set
+     */
+    public void setTrustStorePassword(String trustStorePassword) {
+        this.trustStorePassword = trustStorePassword;
+    }
+
+    /**
+     * @param storeType
+     *        the format that the store files are encoded in.
+     */
+    public void setStoreType(String storeType) {
+        setKeyStoreType(storeType);
+        setTrustStoreType(storeType);
+    }
+
+    /**
+     * @return the keyStoreType
+     */
+    public String getKeyStoreType() {
+        return keyStoreType;
+    }
+
+    /**
+     * @param keyStoreType
+     *        the format that the keyStore file is encoded in
+     */
+    public void setKeyStoreType(String keyStoreType) {
+        this.keyStoreType = keyStoreType;
+    }
+
+    /**
+     * @return the trustStoreType
+     */
+    public String getTrustStoreType() {
+        return trustStoreType;
+    }
+
+    /**
+     * @param trustStoreType
+     *        the format that the trustStore file is encoded in
+     */
+    public void setTrustStoreType(String trustStoreType) {
+        this.trustStoreType = trustStoreType;
+    }
+
+    /**
+     * @return the enabledCipherSuites
+     */
+    public String[] getEnabledCipherSuites() {
+        return enabledCipherSuites;
+    }
+
+    /**
+     * @param enabledCipherSuites the enabledCipherSuites to set
+     */
+    public void setEnabledCipherSuites(String[] enabledCipherSuites) {
+        this.enabledCipherSuites = enabledCipherSuites;
+    }
+
+    /**
+     * @return the disabledCipherSuites
+     */
+    public String[] getDisabledCipherSuites() {
+        return disabledCipherSuites;
+    }
+
+    /**
+     * @param disabledCipherSuites the disabledCipherSuites to set
+     */
+    public void setDisabledCipherSuites(String[] disabledCipherSuites) {
+        this.disabledCipherSuites = disabledCipherSuites;
+    }
+
+    /**
+     * @return the enabledProtocols or null if the defaults should be used
+     */
+    public String[] getEnabledProtocols() {
+        return enabledProtocols;
+    }
+
+    /**
+     * The protocols to be set as enabled.
+     *
+     * @param enabledProtocols the enabled protocols to set, or null if the defaults should be used.
+     */
+    public void setEnabledProtocols(String[] enabledProtocols) {
+        this.enabledProtocols = enabledProtocols;
+    }
+
+    /**
+     * @return the protocols to disable or null if none should be
+     */
+    public String[] getDisabledProtocols() {
+        return disabledProtocols;
+    }
+
+    /**
+     * The protocols to be disable.
+     *
+     * @param disabledProtocols the protocols to disable, or null if none should be.
+     */
+    public void setDisabledProtocols(String[] disabledProtocols) {
+        this.disabledProtocols = disabledProtocols;
+    }
+
+    /**
+    * @return the context protocol to use
+    */
+    public String getContextProtocol() {
+        return contextProtocol;
+    }
+
+    /**
+     * The protocol value to use when creating an SSLContext via
+     * SSLContext.getInstance(protocol).
+     *
+     * @param contextProtocol the context protocol to use.
+     */
+    public void setContextProtocol(String contextProtocol) {
+        this.contextProtocol = contextProtocol;
+    }
+
+    /**
+     * @return the trustAll
+     */
+    public boolean isTrustAll() {
+        return trustAll;
+    }
+
+    /**
+     * @param trustAll the trustAll to set
+     */
+    public void setTrustAll(boolean trustAll) {
+        this.trustAll = trustAll;
+    }
+
+    /**
+     * @return the verifyHost
+     */
+    public boolean isVerifyHost() {
+        return verifyHost;
+    }
+
+    /**
+     * @param verifyHost the verifyHost to set
+     */
+    public void setVerifyHost(boolean verifyHost) {
+        this.verifyHost = verifyHost;
+    }
+
+    /**
+     * @return the key alias
+     */
+    public String getKeyAlias() {
+        return keyAlias;
+    }
+
+    /**
+     * @param keyAlias the key alias to use
+     */
+    public void setKeyAlias(String keyAlias) {
+        this.keyAlias = keyAlias;
+    }
+
+    public void setSslContextOverride(SSLContext sslContextOverride) {
+        this.sslContextOverride = sslContextOverride;
+    }
+
+    public SSLContext getSslContextOverride() {
+        return sslContextOverride;
+    }
+
+    public Map<String, String> getHttpHeaders() {
+        return httpHeaders;
+    }
+
+    /**
+     * @return the configuration that controls if the server requires client authentication.
+     */
+    public boolean isNeedClientAuth() {
+        return needClientAuth;
+    }
+
+    /**
+     * @param needClientAuth
+     *      the needClientAuth should the server require client authentication.
+     */
+    public void setNeedClientAuth(boolean needClientAuth) {
+        this.needClientAuth = needClientAuth;
+    }
+
+    /**
+     * @return the true if this server requires SSL connections
+     */
+    public boolean isSecure() {
+        return secure;
+    }
+
+    /**
+     * @param secure
+     *      should the sever require SSL connections.
+     */
+    public void setSecure(boolean secure) {
+        this.secure = secure;
+    }
+
+    /**
+     * @return true if this server operates over a WebSocket connection.
+     */
+    public boolean isUseWebSockets() {
+        return useWebSockets;
+    }
+
+    /**
+     * @param useWebSockets
+     *      Is this a WebSocket based server.
+     */
+    public void setUseWebSockets(boolean useWebSockets) {
+        this.useWebSockets = useWebSockets;
+    }
+
+    public void setFragmentWrites(boolean fragmentWrites) {
+        this.fragmentWebSocketWrites = fragmentWrites;
+    }
+
+    public boolean isFragmentWrites() {
+        return fragmentWebSocketWrites;
+    }
+
+    public String getWebSocketPath() {
+        return webSocketPath;
+    }
+
+    public void setWebSocketPath(String webSocketPath) {
+        this.webSocketPath = webSocketPath;
+    }
+
+    public int getWebSocketMaxFrameSize() {
+        return webSocketMaxFrameSize;
+    }
+
+    public void setWebSocketMaxFrameSize(int webSocketMaxFrameSize) {
+        this.webSocketMaxFrameSize = webSocketMaxFrameSize;
+    }
+
+    protected ProtonTestClientOptions copyOptions(ProtonTestClientOptions copy) {
+        copy.setReceiveBufferSize(getReceiveBufferSize());
+        copy.setSendBufferSize(getSendBufferSize());
+        copy.setSoLinger(getSoLinger());
+        copy.setSoTimeout(getSoTimeout());
+        copy.setConnectTimeout(getConnectTimeout());
+        copy.setTcpKeepAlive(isTcpKeepAlive());
+        copy.setTcpNoDelay(isTcpNoDelay());
+        copy.setTrafficClass(getTrafficClass());
+        copy.setTraceBytes(isTraceBytes());
+        copy.setKeyStoreLocation(getKeyStoreLocation());
+        copy.setKeyStorePassword(getKeyStorePassword());
+        copy.setTrustStoreLocation(getTrustStoreLocation());
+        copy.setTrustStorePassword(getTrustStorePassword());
+        copy.setKeyStoreType(getKeyStoreType());
+        copy.setTrustStoreType(getTrustStoreType());
+        copy.setEnabledCipherSuites(getEnabledCipherSuites());
+        copy.setDisabledCipherSuites(getDisabledCipherSuites());
+        copy.setEnabledProtocols(getEnabledProtocols());
+        copy.setDisabledProtocols(getDisabledProtocols());
+        copy.setTrustAll(isTrustAll());
+        copy.setVerifyHost(isVerifyHost());
+        copy.setKeyAlias(getKeyAlias());
+        copy.setContextProtocol(getContextProtocol());
+        copy.setSslContextOverride(getSslContextOverride());
+        copy.setLocalAddress(getLocalAddress());
+        copy.setLocalPort(getLocalPort());
+        copy.setSecure(isSecure());
+        copy.setNeedClientAuth(isNeedClientAuth());
+        copy.setUseWebSockets(isUseWebSockets());
+        copy.setFragmentWrites(isFragmentWrites());
+        copy.setWebSocketPath(getWebSocketPath());
+        copy.setWebSocketMaxFrameSize(getWebSocketMaxFrameSize());
+
+        return copy;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestConnector.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestConnector.java
new file mode 100644
index 0000000..c85dd50
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestConnector.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+
+/**
+ * An in VM single threaded test driver used for testing Engine implementations
+ * where all test operations will take place in a single thread of control.
+ *
+ * This class in mainly intended for use in JUnit tests of an Engine implementation
+ * and not for use by client implementations where a socket based test peer would be
+ * a more appropriate choice.
+ */
+public class ProtonTestConnector extends ProtonTestPeer implements Consumer<ByteBuffer> {
+
+    private final AMQPTestDriver driver;
+    private final Consumer<ByteBuffer> inputConsumer;
+
+    public ProtonTestConnector(Consumer<ByteBuffer> frameSink) {
+        this.driver = new AMQPTestDriver(getPeerName(), (frame) -> {
+            processDriverOutput(frame);
+        }, null);
+
+        this.inputConsumer = frameSink;
+    }
+
+    @Override
+    public String getPeerName() {
+        return "InVMConnector";
+    }
+
+    @Override
+    public void accept(ByteBuffer frame) {
+        if (closed.get()) {
+            throw new UncheckedIOException("Closed driver is not accepting any new input", new IOException());
+        } else {
+            driver.accept(frame);
+        }
+    }
+
+    @Override
+    public AMQPTestDriver getDriver() {
+        return driver;
+    }
+
+    //----- Internal implementation which can be overridden
+
+    @Override
+    protected void processCloseRequest() {
+        // nothing to do in this peer implementation.
+    }
+
+    @Override
+    protected void processDriverOutput(ByteBuffer frame) {
+        inputConsumer.accept(frame);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestPeer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestPeer.java
new file mode 100644
index 0000000..4a1ac4e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestPeer.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.test.driver.actions.ConnectionDropAction;
+
+/**
+ * Abstract base class that is implemented by all the AMQP v1.0 test peer
+ * implementations to provide a consistent interface for the test driver
+ * classes to interact with.
+ */
+public abstract class ProtonTestPeer extends ScriptWriter implements AutoCloseable {
+
+    protected final AtomicBoolean closed = new AtomicBoolean();
+
+    public boolean isClosed() {
+        return closed.get();
+    }
+
+    @Override
+    public void close() {
+        if (closed.compareAndSet(false, true)) {
+            processCloseRequest();
+        }
+    }
+
+    public void waitForScriptToCompleteIgnoreErrors() {
+        getDriver().waitForScriptToCompleteIgnoreErrors();
+    }
+
+    public void waitForScriptToComplete() {
+        getDriver().waitForScriptToComplete();
+    }
+
+    public void waitForScriptToComplete(long timeout) {
+        getDriver().waitForScriptToComplete(timeout);
+    }
+
+    public void waitForScriptToComplete(long timeout, TimeUnit units) {
+        getDriver().waitForScriptToComplete(timeout, units);
+    }
+
+    public int getEmptyFrameCount() {
+        return getDriver().getEmptyFrameCount();
+    }
+
+    public int getPerformativeCount() {
+        return getDriver().getPerformativeCount();
+    }
+
+    public int getSaslPerformativeCount() {
+        return getDriver().getSaslPerformativeCount();
+    }
+
+    /**
+     * Drops the connection to the connected client immediately after the last handler that was
+     * registered before this scripted action is queued.  Adding any additional test scripting to
+     * the test driver will either not be acted on or could cause the wait methods to not return
+     * as they will never be invoked.
+     *
+     * @return this test peer instance.
+     */
+    public ProtonTestPeer dropAfterLastHandler() {
+        getDriver().addScriptedElement(new ConnectionDropAction(this));
+        return this;
+    }
+
+    /**
+     * Drops the connection to the connected client immediately after the last handler that was
+     * registered before this scripted action is queued.  Adding any additional test scripting to
+     * the test driver will either not be acted on or could cause the wait methods to not return
+     * as they will never be invoked.
+     *
+     * @param delay
+     *      The time in milliseconds to wait before running the action after the last handler is run.
+     *
+     * @return this test peer instance.
+     */
+    public ProtonTestPeer dropAfterLastHandler(int delay) {
+        getDriver().addScriptedElement(new ConnectionDropAction(this).afterDelay(delay));
+        return this;
+    }
+
+    protected abstract String getPeerName();
+
+    protected abstract void processCloseRequest();
+
+    protected abstract void processDriverOutput(ByteBuffer frame);
+
+    protected void checkClosed() {
+        if (closed.get()) {
+            throw new IllegalStateException("The test peer is closed");
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestServer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestServer.java
new file mode 100644
index 0000000..dfdfff0
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestServer.java
@@ -0,0 +1,247 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import javax.net.ssl.SSLEngine;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.netty.NettyServer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.EventLoop;
+import io.netty.channel.SimpleChannelInboundHandler;
+
+/**
+ * Netty based AMQP Test Server implementation that can handle inbound connections
+ * and script the expected AMQP frame interchange that should occur for a given test.
+ */
+public class ProtonTestServer extends ProtonTestPeer {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ProtonTestServer.class);
+
+    private final AMQPTestDriver driver;
+    private final NettyTestDriverServer server;
+
+    /**
+     * Creates a Socket Test Peer using all default Server options.
+     */
+    public ProtonTestServer() {
+        this(new ProtonTestServerOptions());
+    }
+
+    @Override
+    public String getPeerName() {
+        return "Server";
+    }
+
+    /**
+     * Creates a Socket Test Peer using the options to configure the deployed server.
+     *
+     * @param options
+     *      The options that control the behavior of the deployed server.
+     */
+    public ProtonTestServer(ProtonTestServerOptions options) {
+        this.driver = new NettyAwareAMQPTestDriver(this::processDriverOutput, this::processDriverAssertion, this::eventLoop);
+        this.server = new NettyTestDriverServer(options);
+    }
+
+    public void start() {
+        checkClosed();
+        try {
+            server.start();
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to start server", e);
+        }
+    }
+
+    public URI getServerURI() {
+        checkClosed();
+        try {
+            return server.getConnectionURI();
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to get connection URI: ", e);
+        }
+    }
+
+    public boolean isAcceptingConnections() {
+        return server.isAcceptingConnections();
+    }
+
+    public boolean isSecure() {
+        return server.isSecureServer();
+    }
+
+    public boolean hasSecureConnection() {
+        return server.hasSecureConnection();
+    }
+
+    public boolean isConnectionVerified() {
+        return server.isPeerVerified();
+    }
+
+    public SSLEngine getConnectionSSLEngine() {
+        return server.getConnectionSSLEngine();
+    }
+
+    public int getConnectionRemotePort() {
+        return server.getClientPort();
+    }
+
+    @Override
+    public AMQPTestDriver getDriver() {
+        return driver;
+    }
+
+    //----- Channel handler that drives IO for the test driver
+
+    private final class NettyTestDriverServer extends NettyServer {
+
+        public NettyTestDriverServer(ProtonTestServerOptions options) {
+            super(options);
+        }
+
+        @Override
+        protected ChannelHandler getServerHandler() {
+            return new SimpleChannelInboundHandler<ByteBuf>() {
+
+                @Override
+                protected void channelRead0(ChannelHandlerContext ctx, ByteBuf input) throws Exception {
+                    LOG.trace("AMQP Test Server Channel read: {}", input);
+
+                    try {
+                        // Create a stable copy to avoid issue with retained buffer slices when input is pooled.
+                        ByteBuf copy = Unpooled.buffer(input.readableBytes());
+                        copy.writeBytes(input.nioBuffer());
+                        input.skipBytes(input.readableBytes());
+
+                        // Driver processes new data and may produce output based on this.
+                        processChannelInput(copy);
+                    } catch (Throwable e) {
+                        LOG.error("Closed AMQP Test server channel due to error: ", e);
+                        ctx.channel().close();
+                    }
+                }
+            };
+        }
+    }
+
+    //----- Test driver Wrapper to ensure actions occur on the event loop
+
+    private final class NettyAwareAMQPTestDriver extends AMQPTestDriver {
+
+        public NettyAwareAMQPTestDriver(Consumer<ByteBuffer> frameConsumer, Consumer<AssertionError> assertionConsumer, Supplier<ScheduledExecutorService> scheduler) {
+            super(getPeerName(), frameConsumer, assertionConsumer, scheduler);
+        }
+
+        // If the send call occurs from a reaction to processing incoming data the
+        // call will be on the event loop but for actions requested by the test that
+        // are directed to happen immediately they will be running on the test thread
+        // and so we direct the resulting action into the event loop to avoid codec or
+        // other driver resources being used on two different threads.
+
+        @Override
+        public void sendAMQPFrame(int channel, DescribedType performative, ByteBuf payload) {
+            EventLoop loop = server.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendAMQPFrame(channel, performative, payload);
+            } else {
+                loop.execute(() -> {
+                    super.sendAMQPFrame(channel, performative, payload);
+                });
+            }
+        }
+
+        @Override
+        public void sendSaslFrame(int channel, DescribedType performative) {
+            EventLoop loop = server.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendSaslFrame(channel, performative);
+            } else {
+                loop.execute(() -> {
+                    super.sendSaslFrame(channel, performative);
+                });
+            }
+        }
+
+        @Override
+        public void sendHeader(AMQPHeader header) {
+            EventLoop loop = server.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendHeader(header);
+            } else {
+                loop.execute(() -> {
+                    super.sendHeader(header);
+                });
+            }
+        }
+
+        @Override
+        public void sendEmptyFrame(int channel) {
+            EventLoop loop = server.eventLoop();
+            if (loop.inEventLoop()) {
+                super.sendEmptyFrame(channel);
+            } else {
+                loop.execute(() -> {
+                    super.sendEmptyFrame(channel);
+                });
+            }
+        }
+    }
+
+    //----- Internal implementation which can be overridden
+
+    @Override
+    protected void processCloseRequest() {
+        try {
+            server.stop();
+        } catch (Throwable e) {
+            LOG.info("Error suppressed on server stop: ", e);
+        }
+    }
+
+    @Override
+    protected void processDriverOutput(ByteBuffer frame) {
+        LOG.trace("AMQP Server Channel writing: {}", frame);
+        server.write(frame);
+    }
+
+    protected void processDriverAssertion(AssertionError error) {
+        LOG.trace("AMQP Server Closing due to error: {}", error.getMessage());
+        close();
+    }
+
+    protected void processChannelInput(ByteBuf input) {
+        LOG.trace("AMQP Server Channel processing: {}", input);
+        driver.accept(input);
+    }
+
+    protected ScheduledExecutorService eventLoop() {
+        return server.eventLoop();
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerOptions.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerOptions.java
new file mode 100644
index 0000000..06b85e7
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerOptions.java
@@ -0,0 +1,649 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLContext;
+
+/**
+ * Encapsulates all the Transport options in one configuration object.
+ */
+public class ProtonTestServerOptions implements Cloneable {
+
+    private static final int SERVER_CHOOSES_PORT = 0;
+
+    public static final int DEFAULT_SEND_BUFFER_SIZE = 64 * 1024;
+    public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_SEND_BUFFER_SIZE;
+    public static final int DEFAULT_TRAFFIC_CLASS = 0;
+    public static final boolean DEFAULT_TCP_NO_DELAY = true;
+    public static final boolean DEFAULT_TCP_KEEP_ALIVE = false;
+    public static final int DEFAULT_SO_LINGER = Integer.MIN_VALUE;
+    public static final int DEFAULT_SO_TIMEOUT = -1;
+    public static final int DEFAULT_SERVER_PORT = SERVER_CHOOSES_PORT;
+    public static final boolean DEFAULT_TRACE_BYTES = false;
+    public static final String DEFAULT_STORE_TYPE = "jks";
+    public static final String DEFAULT_CONTEXT_PROTOCOL = "TLS";
+    public static final boolean DEFAULT_TRUST_ALL = false;
+    public static final boolean DEFAULT_VERIFY_HOST = true;
+    public static final List<String> DEFAULT_DISABLED_PROTOCOLS = Collections.unmodifiableList(Arrays.asList(new String[]{"SSLv2Hello", "SSLv3"}));
+    public static final int DEFAULT_LOCAL_PORT = 0;
+    public static final boolean DEFAULT_USE_WEBSOCKETS = false;
+    public static final boolean DEFAULT_FRAGMENT_WEBSOCKET_WRITES = false;
+    public static final boolean DEFAULT_SECURE_SERVER = false;
+    public static final boolean DEFAULT_NEEDS_CLIENT_AUTH = false;
+
+    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
+    private static final String JAVAX_NET_SSL_KEY_STORE_TYPE = "javax.net.ssl.keyStoreType";
+    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
+    private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType";
+    private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword";
+
+    private int sendBufferSize = DEFAULT_SEND_BUFFER_SIZE;
+    private int receiveBufferSize = DEFAULT_RECEIVE_BUFFER_SIZE;
+    private int trafficClass = DEFAULT_TRAFFIC_CLASS;
+    private int soTimeout = DEFAULT_SO_TIMEOUT;
+    private int soLinger = DEFAULT_SO_LINGER;
+    private boolean tcpKeepAlive = DEFAULT_TCP_KEEP_ALIVE;
+    private boolean tcpNoDelay = DEFAULT_TCP_NO_DELAY;
+    private int serverPort = DEFAULT_SERVER_PORT;
+    private String localAddress;
+    private int localPort = DEFAULT_LOCAL_PORT;
+    private boolean traceBytes = DEFAULT_TRACE_BYTES;
+    private boolean useWebSockets = DEFAULT_USE_WEBSOCKETS;
+    private boolean fragmentWebSocketWrites = DEFAULT_FRAGMENT_WEBSOCKET_WRITES;
+
+    private boolean secure = DEFAULT_SECURE_SERVER;
+    private boolean needClientAuth = DEFAULT_NEEDS_CLIENT_AUTH;
+    private String keyStoreLocation;
+    private String keyStorePassword;
+    private String trustStoreLocation;
+    private String trustStorePassword;
+    private String keyStoreType;
+    private String trustStoreType;
+    private String[] enabledCipherSuites;
+    private String[] disabledCipherSuites;
+    private String[] enabledProtocols;
+    private String[] disabledProtocols = DEFAULT_DISABLED_PROTOCOLS.toArray(new String[0]);
+    private String contextProtocol = DEFAULT_CONTEXT_PROTOCOL;
+
+    private boolean trustAll = DEFAULT_TRUST_ALL;
+    private boolean verifyHost = DEFAULT_VERIFY_HOST;
+    private String keyAlias;
+    private SSLContext sslContextOverride;
+
+    private final Map<String, String> httpHeaders = new HashMap<>();
+
+    public ProtonTestServerOptions() {
+        setKeyStoreLocation(System.getProperty(JAVAX_NET_SSL_KEY_STORE));
+        setKeyStoreType(System.getProperty(JAVAX_NET_SSL_KEY_STORE_TYPE, DEFAULT_STORE_TYPE));
+        setKeyStorePassword(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD));
+        setTrustStoreLocation(System.getProperty(JAVAX_NET_SSL_TRUST_STORE));
+        setTrustStoreType(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_TYPE, DEFAULT_STORE_TYPE));
+        setTrustStorePassword(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD));
+    }
+
+    @Override
+    public ProtonTestServerOptions clone() {
+        return copyOptions(new ProtonTestServerOptions());
+    }
+
+    /**
+     * @return the currently set send buffer size in bytes.
+     */
+    public int getSendBufferSize() {
+        return sendBufferSize;
+    }
+
+    /**
+     * Sets the send buffer size in bytes, the value must be greater than zero
+     * or an {@link IllegalArgumentException} will be thrown.
+     *
+     * @param sendBufferSize
+     *        the new send buffer size for the TCP Transport.
+     *
+     * @return this options instance.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public ProtonTestServerOptions setSendBufferSize(int sendBufferSize) {
+        if (sendBufferSize <= 0) {
+            throw new IllegalArgumentException("The send buffer size must be > 0");
+        }
+
+        this.sendBufferSize = sendBufferSize;
+
+        return this;
+    }
+
+    /**
+     * @return the currently configured receive buffer size in bytes.
+     */
+    public int getReceiveBufferSize() {
+        return receiveBufferSize;
+    }
+
+    /**
+     * Sets the receive buffer size in bytes, the value must be greater than zero
+     * or an {@link IllegalArgumentException} will be thrown.
+     *
+     * @param receiveBufferSize
+     *        the new receive buffer size for the TCP Transport.
+     *
+     * @return this options instance.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public ProtonTestServerOptions setReceiveBufferSize(int receiveBufferSize) {
+        if (receiveBufferSize <= 0) {
+            throw new IllegalArgumentException("The send buffer size must be > 0");
+        }
+
+        this.receiveBufferSize = receiveBufferSize;
+
+        return this;
+    }
+
+    /**
+     * @return the currently configured traffic class value.
+     */
+    public int getTrafficClass() {
+        return trafficClass;
+    }
+
+    /**
+     * Sets the traffic class value used by the TCP connection, valid
+     * range is between 0 and 255.
+     *
+     * @param trafficClass
+     *        the new traffic class value.
+     *
+     * @return this options instance.
+     *
+     * @throws IllegalArgumentException if the value given is not in the valid range.
+     */
+    public ProtonTestServerOptions setTrafficClass(int trafficClass) {
+        if (trafficClass < 0 || trafficClass > 255) {
+            throw new IllegalArgumentException("Traffic class must be in the range [0..255]");
+        }
+
+        this.trafficClass = trafficClass;
+
+        return this;
+    }
+
+    public int getSoTimeout() {
+        return soTimeout;
+    }
+
+    public ProtonTestServerOptions setSoTimeout(int soTimeout) {
+        this.soTimeout = soTimeout;
+        return this;
+    }
+
+    public boolean isTcpNoDelay() {
+        return tcpNoDelay;
+    }
+
+    public ProtonTestServerOptions setTcpNoDelay(boolean tcpNoDelay) {
+        this.tcpNoDelay = tcpNoDelay;
+        return this;
+    }
+
+    public int getSoLinger() {
+        return soLinger;
+    }
+
+    public ProtonTestServerOptions setSoLinger(int soLinger) {
+        this.soLinger = soLinger;
+        return this;
+    }
+
+    public boolean isTcpKeepAlive() {
+        return tcpKeepAlive;
+    }
+
+    public ProtonTestServerOptions setTcpKeepAlive(boolean keepAlive) {
+        this.tcpKeepAlive = keepAlive;
+        return this;
+    }
+
+    public int getServerPort() {
+        return serverPort;
+    }
+
+    public ProtonTestServerOptions setServerPort(int serverPort) {
+        this.serverPort = serverPort;
+        return this;
+    }
+
+    public String getLocalAddress() {
+        return localAddress;
+    }
+
+    public ProtonTestServerOptions setLocalAddress(String localAddress) {
+        this.localAddress = localAddress;
+        return this;
+    }
+
+    public int getLocalPort() {
+        return localPort;
+    }
+
+    public ProtonTestServerOptions setLocalPort(int localPort) {
+        this.localPort = localPort;
+        return this;
+    }
+
+    /**
+     * @return true if the transport should enable byte tracing
+     */
+    public boolean isTraceBytes() {
+        return traceBytes;
+    }
+
+    /**
+     * Determines if the transport should add a logger for bytes in / out
+     *
+     * @param traceBytes
+     * 		should the transport log the bytes in and out.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setTraceBytes(boolean traceBytes) {
+        this.traceBytes = traceBytes;
+        return this;
+    }
+
+    /**
+     * @return the keyStoreLocation currently configured.
+     */
+    public String getKeyStoreLocation() {
+        return keyStoreLocation;
+    }
+
+    /**
+     * Sets the location on disk of the key store to use.
+     *
+     * @param keyStoreLocation
+     *        the keyStoreLocation to use to create the key manager.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setKeyStoreLocation(String keyStoreLocation) {
+        this.keyStoreLocation = keyStoreLocation;
+        return this;
+    }
+
+    /**
+     * @return the keyStorePassword
+     */
+    public String getKeyStorePassword() {
+        return keyStorePassword;
+    }
+
+    /**
+     * @param keyStorePassword the keyStorePassword to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setKeyStorePassword(String keyStorePassword) {
+        this.keyStorePassword = keyStorePassword;
+        return this;
+    }
+
+    /**
+     * @return the trustStoreLocation
+     */
+    public String getTrustStoreLocation() {
+        return trustStoreLocation;
+    }
+
+    /**
+     * @param trustStoreLocation the trustStoreLocation to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setTrustStoreLocation(String trustStoreLocation) {
+        this.trustStoreLocation = trustStoreLocation;
+        return this;
+    }
+
+    /**
+     * @return the trustStorePassword
+     */
+    public String getTrustStorePassword() {
+        return trustStorePassword;
+    }
+
+    /**
+     * @param trustStorePassword the trustStorePassword to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setTrustStorePassword(String trustStorePassword) {
+        this.trustStorePassword = trustStorePassword;
+        return this;
+    }
+
+    /**
+     * @param storeType
+     *        the format that the store files are encoded in.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setStoreType(String storeType) {
+        setKeyStoreType(storeType);
+        setTrustStoreType(storeType);
+        return this;
+    }
+
+    /**
+     * @return the keyStoreType
+     */
+    public String getKeyStoreType() {
+        return keyStoreType;
+    }
+
+    /**
+     * @param keyStoreType
+     *        the format that the keyStore file is encoded in
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setKeyStoreType(String keyStoreType) {
+        this.keyStoreType = keyStoreType;
+        return this;
+    }
+
+    /**
+     * @return the trustStoreType
+     */
+    public String getTrustStoreType() {
+        return trustStoreType;
+    }
+
+    /**
+     * @param trustStoreType
+     *        the format that the trustStore file is encoded in
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setTrustStoreType(String trustStoreType) {
+        this.trustStoreType = trustStoreType;
+        return this;
+    }
+
+    /**
+     * @return the enabledCipherSuites
+     */
+    public String[] getEnabledCipherSuites() {
+        return enabledCipherSuites;
+    }
+
+    /**
+     * @param enabledCipherSuites the enabledCipherSuites to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setEnabledCipherSuites(String[] enabledCipherSuites) {
+        this.enabledCipherSuites = enabledCipherSuites;
+        return this;
+    }
+
+    /**
+     * @return the disabledCipherSuites
+     */
+    public String[] getDisabledCipherSuites() {
+        return disabledCipherSuites;
+    }
+
+    /**
+     * @param disabledCipherSuites the disabledCipherSuites to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setDisabledCipherSuites(String[] disabledCipherSuites) {
+        this.disabledCipherSuites = disabledCipherSuites;
+        return this;
+    }
+
+    /**
+     * @return the enabledProtocols or null if the defaults should be used
+     */
+    public String[] getEnabledProtocols() {
+        return enabledProtocols;
+    }
+
+    /**
+     * The protocols to be set as enabled.
+     *
+     * @param enabledProtocols the enabled protocols to set, or null if the defaults should be used.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setEnabledProtocols(String[] enabledProtocols) {
+        this.enabledProtocols = enabledProtocols;
+        return this;
+    }
+
+    /**
+     * @return the protocols to disable or null if none should be
+     */
+    public String[] getDisabledProtocols() {
+        return disabledProtocols;
+    }
+
+    /**
+     * The protocols to be disable.
+     *
+     * @param disabledProtocols the protocols to disable, or null if none should be.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setDisabledProtocols(String[] disabledProtocols) {
+        this.disabledProtocols = disabledProtocols;
+        return this;
+    }
+
+    /**
+    * @return the context protocol to use
+    */
+    public String getContextProtocol() {
+        return contextProtocol;
+    }
+
+    /**
+     * The protocol value to use when creating an SSLContext via
+     * SSLContext.getInstance(protocol).
+     *
+     * @param contextProtocol the context protocol to use.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setContextProtocol(String contextProtocol) {
+        this.contextProtocol = contextProtocol;
+        return this;
+    }
+
+    /**
+     * @return the trustAll
+     */
+    public boolean isTrustAll() {
+        return trustAll;
+    }
+
+    /**
+     * @param trustAll the trustAll to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setTrustAll(boolean trustAll) {
+        this.trustAll = trustAll;
+        return this;
+    }
+
+    /**
+     * @return the verifyHost
+     */
+    public boolean isVerifyHost() {
+        return verifyHost;
+    }
+
+    /**
+     * @param verifyHost the verifyHost to set
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setVerifyHost(boolean verifyHost) {
+        this.verifyHost = verifyHost;
+        return this;
+    }
+
+    /**
+     * @return the key alias
+     */
+    public String getKeyAlias() {
+        return keyAlias;
+    }
+
+    /**
+     * @param keyAlias the key alias to use
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setKeyAlias(String keyAlias) {
+        this.keyAlias = keyAlias;
+        return this;
+    }
+
+    public SSLContext getSslContextOverride() {
+        return sslContextOverride;
+    }
+
+    public ProtonTestServerOptions setSslContextOverride(SSLContext sslContextOverride) {
+        this.sslContextOverride = sslContextOverride;
+        return this;
+    }
+
+    public Map<String, String> getHttpHeaders() {
+        return httpHeaders;
+    }
+
+    /**
+     * @return the configuration that controls if the server requires client authentication.
+     */
+    public boolean isNeedClientAuth() {
+        return needClientAuth;
+    }
+
+    /**
+     * @param needClientAuth
+     *      the needClientAuth should the server require client authentication.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setNeedClientAuth(boolean needClientAuth) {
+        this.needClientAuth = needClientAuth;
+        return this;
+    }
+
+    /**
+     * @return the true if this server requires SSL connections
+     */
+    public boolean isSecure() {
+        return secure;
+    }
+
+    /**
+     * @param secure
+     *      should the sever require SSL connections.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setSecure(boolean secure) {
+        this.secure = secure;
+        return this;
+    }
+
+    /**
+     * @return true if this server operates over a WebSocket connection.
+     */
+    public boolean isUseWebSockets() {
+        return useWebSockets;
+    }
+
+    /**
+     * @param useWebSockets
+     *      Is this a WebSocket based server.
+     *
+     * @return this options instance.
+     */
+    public ProtonTestServerOptions setUseWebSockets(boolean useWebSockets) {
+        this.useWebSockets = useWebSockets;
+        return this;
+    }
+
+    public boolean isFragmentWrites() {
+        return fragmentWebSocketWrites;
+    }
+
+    public ProtonTestServerOptions setFragmentWrites(boolean fragmentWrites) {
+        this.fragmentWebSocketWrites = fragmentWrites;
+        return this;
+    }
+
+    protected ProtonTestServerOptions copyOptions(ProtonTestServerOptions copy) {
+        copy.setReceiveBufferSize(getReceiveBufferSize());
+        copy.setSendBufferSize(getSendBufferSize());
+        copy.setSoLinger(getSoLinger());
+        copy.setSoTimeout(getSoTimeout());
+        copy.setTcpKeepAlive(isTcpKeepAlive());
+        copy.setTcpNoDelay(isTcpNoDelay());
+        copy.setTrafficClass(getTrafficClass());
+        copy.setServerPort(getServerPort());
+        copy.setTraceBytes(isTraceBytes());
+        copy.setKeyStoreLocation(getKeyStoreLocation());
+        copy.setKeyStorePassword(getKeyStorePassword());
+        copy.setTrustStoreLocation(getTrustStoreLocation());
+        copy.setTrustStorePassword(getTrustStorePassword());
+        copy.setKeyStoreType(getKeyStoreType());
+        copy.setTrustStoreType(getTrustStoreType());
+        copy.setEnabledCipherSuites(getEnabledCipherSuites());
+        copy.setDisabledCipherSuites(getDisabledCipherSuites());
+        copy.setEnabledProtocols(getEnabledProtocols());
+        copy.setDisabledProtocols(getDisabledProtocols());
+        copy.setTrustAll(isTrustAll());
+        copy.setVerifyHost(isVerifyHost());
+        copy.setKeyAlias(getKeyAlias());
+        copy.setContextProtocol(getContextProtocol());
+        copy.setSslContextOverride(getSslContextOverride());
+        copy.setLocalAddress(getLocalAddress());
+        copy.setLocalPort(getLocalPort());
+        copy.setSecure(isSecure());
+        copy.setNeedClientAuth(isNeedClientAuth());
+        copy.setUseWebSockets(isUseWebSockets());
+        copy.setFragmentWrites(isFragmentWrites());
+
+        return copy;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ReceiverTracker.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ReceiverTracker.java
new file mode 100644
index 0000000..6b632c8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ReceiverTracker.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Link Tracker that manages tracking of the peer Receiver link which will
+ * handle flows and receive transfers to a remote Sender link.
+ */
+public class ReceiverTracker extends LinkTracker {
+
+    public ReceiverTracker(SessionTracker session, Attach attach) {
+        super(session, attach);
+        // TODO Auto-generated constructor stub
+    }
+
+    @Override
+    protected void handleTransfer(Transfer transfer, ByteBuf payload) {
+        // TODO: Update internal state
+    }
+
+    @Override
+    protected void handleFlow(Flow flow) {
+
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java
new file mode 100644
index 0000000..cf8cddd
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java
@@ -0,0 +1,607 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.isA;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.test.driver.actions.AMQPHeaderInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.AttachInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.CloseInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DeclareInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DetachInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DetachLastCoordinatorInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DischargeInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DispositionInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.EmptyFrameInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.EndInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.ExecuteUserCodeAction;
+import org.apache.qpid.protonj2.test.driver.actions.FlowInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.OpenInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.RawBytesInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.SaslChallengeInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.SaslInitInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.SaslMechanismsInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.SaslOutcomeInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.SaslResponseInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.TransferInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslCode;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.expectations.AMQPHeaderExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.AttachExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.BeginExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.CloseExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.DeclareExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.DetachExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.DischargeExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.DispositionExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.EmptyFrameExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.EndExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.FlowExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.OpenExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.SaslChallengeExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.SaslInitExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.SaslMechanismsExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.SaslOutcomeExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.SaslResponseExpectation;
+import org.apache.qpid.protonj2.test.driver.expectations.TransferExpectation;
+
+/**
+ * Class used to create test scripts using the {@link AMQPTestDriver}
+ */
+public abstract class ScriptWriter {
+
+    /**
+     * Implemented in the subclass this method returns the active AMQP test driver
+     * instance being used for the tests.
+     *
+     * @return the {@link AMQPTestDriver} to use for building a test script.
+     */
+    public abstract AMQPTestDriver getDriver();
+
+    //----- AMQP Performative expectations
+
+    public AMQPHeaderExpectation expectAMQPHeader() {
+        AMQPHeaderExpectation expecting = new AMQPHeaderExpectation(AMQPHeader.getAMQPHeader(), getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public OpenExpectation expectOpen() {
+        OpenExpectation expecting = new OpenExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public CloseExpectation expectClose() {
+        CloseExpectation expecting = new CloseExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public BeginExpectation expectBegin() {
+        BeginExpectation expecting = new BeginExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public EndExpectation expectEnd() {
+        EndExpectation expecting = new EndExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public AttachExpectation expectAttach() {
+        AttachExpectation expecting = new AttachExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public DetachExpectation expectDetach() {
+        DetachExpectation expecting = new DetachExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public FlowExpectation expectFlow() {
+        FlowExpectation expecting = new FlowExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public TransferExpectation expectTransfer() {
+        TransferExpectation expecting = new TransferExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public DispositionExpectation expectDisposition() {
+        DispositionExpectation expecting = new DispositionExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public EmptyFrameExpectation expectEmptyFrame() {
+        EmptyFrameExpectation expecting = new EmptyFrameExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    //----- Transaction expectations
+
+    public AttachExpectation expectCoordinatorAttach() {
+        AttachExpectation expecting = new AttachExpectation(getDriver());
+
+        expecting.withRole(Role.SENDER);
+        expecting.withCoordinator(isA(Coordinator.class));
+        expecting.withSource(notNullValue());
+
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public DeclareExpectation expectDeclare() {
+        DeclareExpectation expecting = new DeclareExpectation(getDriver());
+
+        expecting.withHandle(notNullValue());
+        expecting.withDeliveryId(notNullValue());
+        expecting.withDeliveryTag(notNullValue());
+        expecting.withMessageFormat(anyOf(nullValue(), equalTo(0)));
+
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public DischargeExpectation expectDischarge() {
+        DischargeExpectation expecting = new DischargeExpectation(getDriver());
+
+        expecting.withHandle(notNullValue());
+        expecting.withDeliveryId(notNullValue());
+        expecting.withDeliveryTag(notNullValue());
+        expecting.withMessageFormat(anyOf(nullValue(), equalTo(0)));
+
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    //----- SASL performative expectations
+
+    public AMQPHeaderExpectation expectSASLHeader() {
+        AMQPHeaderExpectation expecting = new AMQPHeaderExpectation(AMQPHeader.getSASLHeader(), getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public SaslMechanismsExpectation expectSaslMechanisms() {
+        SaslMechanismsExpectation expecting = new SaslMechanismsExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public SaslInitExpectation expectSaslInit() {
+        SaslInitExpectation expecting = new SaslInitExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public SaslChallengeExpectation expectSaslChallenge() {
+        SaslChallengeExpectation expecting = new SaslChallengeExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public SaslResponseExpectation expectSaslResponse() {
+        SaslResponseExpectation expecting = new SaslResponseExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    public SaslOutcomeExpectation expectSaslOutcome() {
+        SaslOutcomeExpectation expecting = new SaslOutcomeExpectation(getDriver());
+        getDriver().addScriptedElement(expecting);
+        return expecting;
+    }
+
+    //----- Remote operations that happen while running the test script
+
+    public AMQPHeaderInjectAction remoteHeader(byte[] header) {
+        return new AMQPHeaderInjectAction(getDriver(), new AMQPHeader(header));
+    }
+
+    public AMQPHeaderInjectAction remoteHeader(AMQPHeader header) {
+        return new AMQPHeaderInjectAction(getDriver(), header);
+    }
+
+    public OpenInjectAction remoteOpen() {
+        return new OpenInjectAction(getDriver());
+    }
+
+    public CloseInjectAction remoteClose() {
+        return new CloseInjectAction(getDriver());
+    }
+
+    public BeginInjectAction remoteBegin() {
+        return new BeginInjectAction(getDriver());
+    }
+
+    public EndInjectAction remoteEnd() {
+        return new EndInjectAction(getDriver());
+    }
+
+    public AttachInjectAction remoteAttach() {
+        return new AttachInjectAction(getDriver());
+    }
+
+    public DetachInjectAction remoteDetach() {
+        return new DetachInjectAction(getDriver());
+    }
+
+    public DetachInjectAction remoteDetachLastCoordinatorLink() {
+        return new DetachLastCoordinatorInjectAction(getDriver());
+    }
+
+    public FlowInjectAction remoteFlow() {
+        return new FlowInjectAction(getDriver());
+    }
+
+    public TransferInjectAction remoteTransfer() {
+        return new TransferInjectAction(getDriver());
+    }
+
+    public DispositionInjectAction remoteDisposition() {
+        return new DispositionInjectAction(getDriver());
+    }
+
+    public DeclareInjectAction remoteDeclare() {
+        return new DeclareInjectAction(getDriver());
+    }
+
+    public DischargeInjectAction remoteDischarge() {
+        return new DischargeInjectAction(getDriver());
+    }
+
+    public EmptyFrameInjectAction remoteEmptyFrame() {
+        return new EmptyFrameInjectAction(getDriver());
+    }
+
+    public RawBytesInjectAction remoteBytes() {
+        return new RawBytesInjectAction(getDriver());
+    }
+
+    //----- Remote SASL operations that can be scripted during tests
+
+    public SaslInitInjectAction remoteSaslInit() {
+        return new SaslInitInjectAction(getDriver());
+    }
+
+    public SaslMechanismsInjectAction remoteSaslMechanisms() {
+        return new SaslMechanismsInjectAction(getDriver());
+    }
+
+    public SaslChallengeInjectAction remoteSaslChallenge() {
+        return new SaslChallengeInjectAction(getDriver());
+    }
+
+    public SaslResponseInjectAction remoteSaslResponse() {
+        return new SaslResponseInjectAction(getDriver());
+    }
+
+    public SaslOutcomeInjectAction remoteSaslOutcome() {
+        return new SaslOutcomeInjectAction(getDriver());
+    }
+
+    //----- SASL related test expectations
+
+    /**
+     * Creates all the scripted elements needed for a successful SASL Anonymous
+     * connection.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that only
+     * advertises anonymous as the mechanism.  It is expected that the remote will
+     * send a SASL init with the anonymous mechanism selected and the outcome is
+     * predefined as success.  Once done the expectation is added for the AMQP
+     * header to arrive and a header response will be sent.
+     */
+    public void expectSASLAnonymousConnect() {
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms("ANONYMOUS").queue();
+        expectSaslInit().withMechanism("ANONYMOUS");
+        remoteSaslOutcome().withCode(SaslCode.OK).queue();
+        expectAMQPHeader().respondWithAMQPHeader();
+    }
+
+    /**
+     * Creates all the scripted elements needed for a successful SASL Plain
+     * connection.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that only
+     * advertises plain as the mechanism.  It is expected that the remote will
+     * send a SASL init with the plain mechanism selected and the outcome is
+     * predefined as success.  Once done the expectation is added for the AMQP
+     * header to arrive and a header response will be sent.
+     *
+     * @param username
+     *      The user name that is expected in the SASL Plain initial response.
+     * @param password
+     *      The password that is expected in the SASL Plain initial response.
+     */
+    public void expectSASLPlainConnect(String username, String password) {
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms("PLAIN").queue();
+        expectSaslInit().withMechanism("PLAIN").withInitialResponse(saslPlainInitialResponse(username, password));
+        remoteSaslOutcome().withCode(SaslCode.OK).queue();
+        expectAMQPHeader().respondWithAMQPHeader();
+    }
+
+    /**
+     * Creates all the scripted elements needed for a successful SASL XOAUTH2
+     * connection.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that only
+     * advertises XOAUTH2 as the mechanism.  It is expected that the remote will
+     * send a SASL init with the XOAUTH2 mechanism selected and the outcome is
+     * predefined as success.  Once done the expectation is added for the AMQP
+     * header to arrive and a header response will be sent.
+     *
+     * @param username
+     *      The user name that is expected in the SASL initial response.
+     * @param password
+     *      The password that is expected in the SASL initial response.
+     */
+    public void expectSaslXOauth2Connect(String username, String password) {
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms("XOAUTH2").queue();
+        expectSaslInit().withMechanism("XOAUTH2").withInitialResponse(saslXOauth2InitialResponse(username, password));
+        remoteSaslOutcome().withCode(SaslCode.OK).queue();
+        expectAMQPHeader().respondWithAMQPHeader();
+    }
+
+    /**
+     * Creates all the scripted elements needed for a failed SASL Plain
+     * connection.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that only
+     * advertises plain as the mechanism.  It is expected that the remote will
+     * send a SASL init with the plain mechanism selected and the outcome is
+     * predefined failing the exchange.
+     *
+     * @param saslCode
+     *      The SASL code that indicates which failure the remote will be sent.
+     */
+    public void expectFailingSASLPlainConnect(byte saslCode) {
+        expectFailingSASLPlainConnect(saslCode, "PLAIN");
+    }
+
+    /**
+     * Creates all the scripted elements needed for a failed SASL Plain
+     * connection.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that only
+     * advertises plain as the mechanism.  It is expected that the remote will
+     * send a SASL init with the plain mechanism selected and the outcome is
+     * predefined failing the exchange.
+     *
+     * @param saslCode
+     *      The SASL code that indicates which failure the remote will be sent.
+     * @param offeredMechanisms
+     *      The set of mechanisms that the server should offer in the SASL Mechanisms frame
+     */
+    public void expectFailingSASLPlainConnect(byte saslCode, String... offeredMechanisms) {
+        assertTrue(Arrays.asList(offeredMechanisms).contains("PLAIN"));
+
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
+        expectSaslInit().withMechanism("PLAIN");
+
+        if (saslCode <= 0 || saslCode > SaslCode.SYS_TEMP.ordinal()) {
+            throw new IllegalArgumentException("SASL Code should indicate a failure");
+        }
+
+        remoteSaslOutcome().withCode(SaslCode.valueOf(saslCode)).queue();
+    }
+
+    /**
+     * Creates all the scripted elements needed for a successful SASL EXTERNAL
+     * connection.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that only
+     * advertises EXTERNAL as the mechanism.  It is expected that the remote will
+     * send a SASL init with the EXTERNAL mechanism selected and the outcome is
+     * predefined as success.  Once done the expectation is added for the AMQP
+     * header to arrive and a header response will be sent.
+     */
+    public void expectSaslExternalConnect() {
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms("EXTERNAL").queue();
+        expectSaslInit().withMechanism("EXTERNAL").withInitialResponse(new byte[0]);
+        remoteSaslOutcome().withCode(SaslCode.OK).queue();
+        expectAMQPHeader().respondWithAMQPHeader();
+    }
+
+    /**
+     * Creates all the scripted elements needed for a SASL exchange with the offered
+     * mechanisms but the client should fail if configured such that it cannot match
+     * any of those to its own available mechanisms.
+     *
+     * @param offeredMechanisms
+     *      The set of SASL Mechanisms to advertise as available on the peer.
+     */
+    public void expectSaslMechanismNegotiationFailure(String... offeredMechanisms) {
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
+    }
+
+    /**
+     * Creates all the scripted elements needed for a SASL exchange with the offered
+     * mechanisms with the expectation that the client will respond with the provided
+     * mechanism and then the server will fail the exchange with the auth failed code.
+     *
+     * @param offeredMechanisms
+     *      The set of SASL Mechanisms to advertise as available on the peer.
+     * @param chosenMechanism
+     *      The SASL Mechanism that the client should select and respond with.
+     */
+    public void expectSaslConnectThatAlwaysFailsAuthentication(String[] offeredMechanisms, String chosenMechanism) {
+        expectSASLHeader().respondWithSASLPHeader();
+        remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
+        expectSaslInit().withMechanism(chosenMechanism);
+        remoteSaslOutcome().withCode(SaslCode.AUTH).queue();
+    }
+
+    //----- Utility methods for tests writing raw scripted SASL tests
+
+    public byte[] saslPlainInitialResponse(String username, String password) {
+        byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
+        byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
+        byte[] initialResponse = new byte[usernameBytes.length+passwordBytes.length+2];
+        System.arraycopy(usernameBytes, 0, initialResponse, 1, usernameBytes.length);
+        System.arraycopy(passwordBytes, 0, initialResponse, 2 + usernameBytes.length, passwordBytes.length);
+
+        return initialResponse;
+    }
+
+    public byte[] saslXOauth2InitialResponse(String username, String password) {
+        byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
+        byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
+        byte[] initialResponse = new byte[usernameBytes.length+passwordBytes.length+20];
+
+        System.arraycopy("user=".getBytes(StandardCharsets.US_ASCII), 0, initialResponse, 0, 5);
+        System.arraycopy(usernameBytes, 0, initialResponse, 5, usernameBytes.length);
+        initialResponse[5 + usernameBytes.length] = 1;
+        System.arraycopy("auth=Bearer ".getBytes(StandardCharsets.US_ASCII), 0, initialResponse, 6+usernameBytes.length, 12);
+        System.arraycopy(passwordBytes, 0, initialResponse, 18 + usernameBytes.length, passwordBytes.length);
+        initialResponse[initialResponse.length - 2] = 1;
+        initialResponse[initialResponse.length - 1] = 1;
+
+        return initialResponse;
+    }
+
+    //----- Smart Scripted Response Actions
+
+    /**
+     * Creates a Begin response for the last session Begin that was received and fills in the Begin
+     * fields based on values from the remote.  The caller can further customize the Begin that is
+     * emitted by using the various with methods to assign values to the fields in the Begin.
+     *
+     * @return a new {@link BeginInjectAction} that can be queued or sent immediately.
+     *
+     * @throws IllegalStateException if no Begin has yet been received from the remote.
+     */
+    public BeginInjectAction respondToLastBegin() {
+        BeginInjectAction response = new BeginInjectAction(getDriver());
+
+        SessionTracker session = getDriver().sessions().getLastRemotelyOpenedSession();
+        if (session == null) {
+            throw new IllegalStateException("Cannot create response to Begin before one has been received.");
+        }
+
+        // Populate the response using data in the locally opened session, script can override this after return.
+        response.withRemoteChannel(session.getRemoteChannel());
+
+        return response;
+    }
+
+    /**
+     * Creates a Attach response for the last link Attach that was received and fills in the Attach
+     * fields based on values from the remote.  The caller can further customize the Attach that is
+     * emitted by using the various with methods to assign values to the fields in the Attach.
+     *
+     * @return a new {@link AttachInjectAction} that can be queued or sent immediately.
+     *
+     * @throws IllegalStateException if no Attach has yet been received from the remote.
+     */
+    public AttachInjectAction respondToLastAttach() {
+        AttachInjectAction response = new AttachInjectAction(getDriver());
+
+        LinkTracker link = getDriver().sessions().getLastRemotelyOpenedSession().getLastOpenedLink();
+        if (link == null) {
+            throw new IllegalStateException("Cannot create response to Attach before one has been received.");
+        }
+
+        // Populate the response using data in the locally opened link, script can override this after return.
+        response.onChannel(link.getSession().getLocalChannel());
+        response.withHandle(link.getHandle());
+        response.withName(link.getName());
+        response.withRole(link.getRole());
+        response.withSndSettleMode(link.getSenderSettleMode());
+        response.withRcvSettleMode(link.getReceiverSettleMode());
+
+        if (link.getSource() != null) {
+            response.withSource(new Source(link.getSource()));
+            if (Boolean.TRUE.equals(link.getSource().getDynamic())) {
+                response.withSource().withAddress(UUID.randomUUID().toString());
+            }
+        }
+        if (link.getTarget() != null) {
+            response.withTarget(new Target(link.getTarget()));
+            if (Boolean.TRUE.equals(link.getTarget().getDynamic())) {
+                response.withTarget().withAddress(UUID.randomUUID().toString());
+            }
+        }
+        if (link.getCoordinator() != null) {
+            response.withTarget(new Coordinator(link.getCoordinator()));
+        }
+
+        if (response.getPerformative().getInitialDeliveryCount() == null) {
+            if (link.getRole() == Role.SENDER) {
+                response.withInitialDeliveryCount(0);
+            }
+        }
+
+        return response;
+    }
+
+    //----- Out of band script actions for user code
+
+    public ExecuteUserCodeAction execute(Runnable action) {
+        return new ExecuteUserCodeAction(getDriver(), action);
+    }
+
+    //----- Immediate operations performed outside the test script
+
+    public void fire(AMQPHeader header) {
+        getDriver().sendHeader(header);
+    }
+
+    public void fireAMQP(DescribedType performative) {
+        getDriver().sendAMQPFrame(0, performative, null);
+    }
+
+    public void fireSASL(DescribedType performative) {
+        getDriver().sendSaslFrame(0, performative);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedAction.java
new file mode 100644
index 0000000..36b4ace
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedAction.java
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslChallenge;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslInit;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslMechanisms;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslResponse;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Close;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Entry in the test script that produces some output to be sent to the AMQP
+ * peer under test.
+ */
+public interface ScriptedAction extends ScriptedElement {
+
+    @Override
+    default ScriptEntryType getType() {
+        return ScriptEntryType.ACTION;
+    }
+
+    /**
+     * Runs the scripted action on its associated test driver immediately
+     * regardless of any queued tasks or expected inputs.
+     *
+     * @return this scripted action.
+     */
+    ScriptedAction now();
+
+    /**
+     * Runs the scripted action on its associated test driver immediately
+     * following the given wait time regardless of any queued tasks or
+     * expected inputs.
+     *
+     * @param waitTime
+     *      Time in milliseconds to wait before running this action.
+     *
+     * @return this scripted action.
+     */
+    ScriptedAction later(int waitTime);
+
+    /**
+     * Queues the scripted action for later run after any preceding scripted
+     * elements are performed.
+     *
+     * @return this scripted action.
+     */
+    ScriptedAction queue();
+
+    /**
+     * Triggers the action to be performed on the given {@link Consumer}.
+     *
+     * @param driver
+     *      The test driver that is managing the test
+     *
+     * @return this scripted action.
+     */
+    ScriptedAction perform(AMQPTestDriver driver);
+
+    // By default the Action type is not expecting to be triggered by an incoming
+    // AMQP frame so in all these cases we fail because the script was wrong or the
+    // remote sent something we didn't expect.
+
+    @Override
+    default void handleAMQPHeader(AMQPHeader header, AMQPTestDriver context) {
+        throw new AssertionError("AMQP Header arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleSASLHeader(AMQPHeader header, AMQPTestDriver context) {
+        throw new AssertionError("SASL Header arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleOpen(Open open, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Open arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleBegin(Begin begin, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Begin arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleAttach(Attach attach, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Attach arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleFlow(Flow flow, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Flow arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleTransfer(Transfer transfer, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Transfer arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleDisposition(Disposition disposition, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Disposition arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleDetach(Detach detach, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Detach arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleEnd(End end, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("End arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleClose(Close close, ByteBuf payload, int channel, AMQPTestDriver context) {
+        throw new AssertionError("Close arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleMechanisms(SaslMechanisms saslMechanisms, AMQPTestDriver context) {
+        throw new AssertionError("SaslMechanisms arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleInit(SaslInit saslInit, AMQPTestDriver context) {
+        throw new AssertionError("SaslInit arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleChallenge(SaslChallenge saslChallenge, AMQPTestDriver context) {
+        throw new AssertionError("SaslChallenge arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleResponse(SaslResponse saslResponse, AMQPTestDriver context) {
+        throw new AssertionError("SaslResponse arrived when expecting to perform an action");
+    }
+
+    @Override
+    default void handleOutcome(SaslOutcome saslOutcome, AMQPTestDriver context) {
+        throw new AssertionError("SaslOutcome arrived when expecting to perform an action");
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedElement.java
new file mode 100644
index 0000000..a088c5c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedElement.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.codec.transport.PerformativeDescribedType;
+
+/**
+ * Root of scripted entries in the test driver script.
+ */
+public interface ScriptedElement extends AMQPHeader.HeaderHandler<AMQPTestDriver>,
+                                         PerformativeDescribedType.PerformativeHandler<AMQPTestDriver>,
+                                         SaslDescribedType.SaslPerformativeHandler<AMQPTestDriver> {
+
+    enum ScriptEntryType {
+        EXPECTATION,
+        ACTION
+    }
+
+    /**
+     * @return the type of script entry this instance represents
+     */
+    ScriptEntryType getType();
+
+    /**
+     * @return true if this element represents an optional part of the script.
+     */
+    default boolean isOptional() {
+        return false;
+    }
+
+    /**
+     * @return a {@link ScriptedAction} to perform after the element has been performed.
+     */
+    default ScriptedAction performAfterwards() {
+        return null;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedExpectation.java
new file mode 100644
index 0000000..9d0e9ae
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptedExpectation.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+/**
+ * Entry in the test script that defines an expected output from the AMQP source being
+ * tested.
+ */
+public interface ScriptedExpectation extends ScriptedElement {
+
+    @Override
+    default ScriptEntryType getType() {
+        return ScriptEntryType.EXPECTATION;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/SenderTracker.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/SenderTracker.java
new file mode 100644
index 0000000..1d9c685
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/SenderTracker.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Link Tracker that manages tracking of the peer Sender link which will
+ * handle flows and initiate transfers to a remote Receiver link.
+ */
+public class SenderTracker extends LinkTracker {
+
+    public SenderTracker(SessionTracker session, Attach attach) {
+        super(session, attach);
+    }
+
+    @Override
+    protected void handleTransfer(Transfer transfer, ByteBuf payload) {
+        throw new AssertionError("Sender links cannot process incoming Transfers");
+    }
+
+    @Override
+    protected void handleFlow(Flow flow) {
+        // TODO Auto-generated method stub
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/SessionTracker.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/SessionTracker.java
new file mode 100644
index 0000000..d74de37
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/SessionTracker.java
@@ -0,0 +1,247 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Tracks information related to an opened Session and its various links
+ */
+public class SessionTracker {
+
+    private final Deque<LinkTracker> remoteSenders = new ArrayDeque<>();
+    private final Deque<LinkTracker> remoteReceivers = new ArrayDeque<>();
+
+    private final Map<UnsignedInteger, LinkTracker> trackerMap = new LinkedHashMap<>();
+
+    private UnsignedShort localChannel;
+    private UnsignedShort remoteChannel;
+    private UnsignedInteger nextIncomingId;
+    private UnsignedInteger nextOutgoingId;
+    private Begin remoteBegin;
+    private Begin localBegin;
+    private End remoteEnd;
+    private End localEnd;
+    private LinkTracker lastOpenedLink;
+    private LinkTracker lastOpenedCoordinatorLink;
+
+    private final AMQPTestDriver driver;
+
+    public SessionTracker(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    public AMQPTestDriver getDriver() {
+        return driver;
+    }
+
+    public LinkTracker getLastOpenedLink() {
+        return lastOpenedLink;
+    }
+
+    public LinkTracker getLastOpenedCoordinatorLink() {
+        return lastOpenedCoordinatorLink;
+    }
+
+    public LinkTracker getLastOpenedRemoteSender() {
+        return remoteSenders.getLast();
+    }
+
+    public LinkTracker getLastOpenedRemoteReceiver() {
+        return remoteReceivers.getLast();
+    }
+
+    public End getRemoteEnd() {
+        return remoteEnd;
+    }
+
+    public End getLocalEnd() {
+        return localEnd;
+    }
+
+    //----- Session specific access which can provide details for expectations
+
+    public Begin getRemoteBegin() {
+        return remoteBegin;
+    }
+
+    public Begin getLocalBegin() {
+        return localBegin;
+    }
+
+    public UnsignedShort getRemoteChannel() {
+        return remoteChannel;
+    }
+
+    public UnsignedShort getLocalChannel() {
+        return localChannel;
+    }
+
+    public UnsignedInteger getNextIncomingId() {
+        return nextIncomingId;
+    }
+
+    public UnsignedInteger getNextOutgoingId() {
+        return nextOutgoingId;
+    }
+
+    //----- Handle performatives and update session state
+
+    public SessionTracker handleBegin(Begin remoteBegin, UnsignedShort remoteChannel) {
+        this.remoteBegin = remoteBegin;
+        this.remoteChannel = remoteChannel;
+        this.nextIncomingId = remoteBegin.getNextOutgoingId();
+
+        return this;
+    }
+
+    public SessionTracker handleLocalBegin(Begin localBegin, UnsignedShort localChannel) {
+        this.localBegin = localBegin;
+        this.localChannel = localChannel;
+        this.nextOutgoingId = localBegin.getNextOutgoingId();
+
+        return this;
+    }
+
+    public SessionTracker handleEnd(End end) {
+        this.remoteEnd = end;
+        return this;
+    }
+
+    public SessionTracker handleLocalEnd(End end) {
+        this.localEnd = end;
+        return this;
+    }
+
+    public LinkTracker handleRemoteAttach(Attach attach) {
+        LinkTracker linkTracker = trackerMap.get(attach.getHandle());
+
+        // We only populate these remote value here, never in the local side processing
+        // this implies that we need to check if this was remotely initiated and create
+        // the link tracker if none exists yet
+        // TODO: These SenderTracker and ReceiverTracker inversions are confusing and probably
+        //       not going to work for future enhancements.
+        if (attach.getRole().equals(Role.SENDER.getValue())) {
+            if (linkTracker == null) {
+                linkTracker = new ReceiverTracker(this, attach);
+            }
+            remoteSenders.add(linkTracker);
+        } else {
+            if (linkTracker == null) {
+                linkTracker = new SenderTracker(this, attach);
+            }
+            remoteReceivers.add(linkTracker);
+        }
+
+        if (attach.getTarget() instanceof Coordinator) {
+            lastOpenedCoordinatorLink = linkTracker;
+            driver.sessions().setLastOpenedCoordinator(lastOpenedCoordinatorLink);
+        }
+
+        lastOpenedLink = linkTracker;
+        trackerMap.put(attach.getHandle(), linkTracker);
+
+        return linkTracker;
+    }
+
+    public LinkTracker handleLocalAttach(Attach attach) {
+        LinkTracker linkTracker = trackerMap.get(attach.getHandle());
+
+        // Create a tracker for the local side to use to respond to remote
+        // performative or to use when invoking local actions.
+        if (linkTracker == null) {
+            if (attach.getRole().equals(Role.SENDER.getValue())) {
+                linkTracker = new SenderTracker(this, attach);
+            } else {
+                linkTracker = new ReceiverTracker(this, attach);
+            }
+        }
+
+        lastOpenedLink = linkTracker;
+        trackerMap.put(attach.getHandle(), linkTracker);
+
+        return linkTracker;
+    }
+
+    public LinkTracker handleRemoteDetach(Detach detach) {
+        LinkTracker tracker = trackerMap.get(detach.getHandle());
+
+        if (tracker != null) {
+            remoteSenders.remove(tracker);
+            remoteReceivers.remove(tracker);
+        }
+
+        return tracker;
+    }
+
+    public LinkTracker handleLocalDetach(Detach detach) {
+        LinkTracker tracker = trackerMap.get(detach.getHandle());
+
+        // TODO: Cleanup local state when we start tracking both sides.
+
+        return tracker;
+    }
+
+    public LinkTracker handleTransfer(Transfer transfer, ByteBuf payload) {
+        LinkTracker tracker = trackerMap.get(transfer.getHandle());
+
+        tracker.handleTransfer(transfer, payload);
+        // TODO - Update session state based on transfer
+
+        return tracker;
+    }
+
+    public LinkTracker handleFlow(Flow flow) {
+        LinkTracker tracker = null;
+
+        if (flow.getHandle() != null) {
+            tracker = trackerMap.get(flow.getHandle());
+            tracker.handleFlow(flow);
+        }
+
+        return tracker;
+    }
+
+    public UnsignedInteger findFreeLocalHandle() {
+        final UnsignedInteger HANDLE_MAX = localBegin.getHandleMax() != null ? localBegin.getHandleMax() : UnsignedInteger.MAX_VALUE;
+
+        for (long i = 0; i <= HANDLE_MAX.longValue(); ++i) {
+            final UnsignedInteger handle = UnsignedInteger.valueOf(i);
+            if (!trackerMap.containsKey(handle)) {
+                return handle;
+            }
+        }
+
+        throw new IllegalStateException("no local handle available for allocation");
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/TypeConversionRegistry.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/TypeConversionRegistry.java
new file mode 100644
index 0000000..e45b153
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/TypeConversionRegistry.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+/**
+ * Registry of external application converters from an external AMQP type to the internal test
+ * driver types.
+ */
+public class TypeConversionRegistry {
+
+    public enum AMQPTypes {
+        BINARY,
+        DECIMAL128,
+        DECIMAL64,
+        DECIMAL32,
+        SYMBOL,
+        UNSIGNED_BYTE,
+        UNSIGNED_SHORT,
+        UNSIGNED_INT,
+        UNSIGNED_LONG,
+        ACCEPTED,
+        AMQP_SEQUENCE,
+        AMQP_VALUE,
+        APPLICATION_PROPERTIES,
+        DATA,
+        DELETE_ON_CLOSE,
+        DELETE_ON_NO_LINKS,
+        DELETE_ON_NO_LINKS_OR_NO_MESSAGES,
+        DELETE_ON_NO_MESSAGES,
+        DELIVERY_ANNOTATIONS,
+        FOOTER,
+        HEADER,
+        MESSAGE_ANNOTATIONS,
+        MODIFIED,
+        PROPERTIES,
+        RECEIVED,
+        REJECTED,
+        RELEASED,
+        SOURCE,
+        TARGET,
+        ATTACH,
+        BEGIN,
+        CLOSE,
+        DETACH,
+        DISPOSITION,
+        END,
+        ERROR_CONDITION,
+        FLOW,
+        OPEN,
+        TRANSFER,
+        COORDINATOR,
+        DECLARE,
+        DECLARED,
+        DISCHARGE,
+        TRANSACTIONAL_STATE,
+        SASL_CHALLENGE,
+        SASL_INIT,
+        SASL_MECHANISM,
+        SASL_OUTCOME,
+        SASL_RESPONSE
+    }
+
+    TypeConversionRegistry() {
+        // TODO Auto-generated constructor stub
+    }
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AMQPHeaderInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AMQPHeaderInjectAction.java
new file mode 100644
index 0000000..bc9c930
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AMQPHeaderInjectAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+
+/**
+ * AMQP Header injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class AMQPHeaderInjectAction implements ScriptedAction {
+
+    private final AMQPTestDriver driver;
+    private final AMQPHeader header;
+
+    public AMQPHeaderInjectAction(AMQPTestDriver driver, AMQPHeader header) {
+        this.header = header;
+        this.driver = driver;
+    }
+
+    @Override
+    public AMQPHeaderInjectAction perform(AMQPTestDriver driver) {
+        driver.sendHeader(header);
+        return this;
+    }
+
+    @Override
+    public AMQPHeaderInjectAction now() {
+        perform(driver);
+        return this;
+    }
+
+    @Override
+    public AMQPHeaderInjectAction later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public AMQPHeaderInjectAction queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AbstractPerformativeInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AbstractPerformativeInjectAction.java
new file mode 100644
index 0000000..4f2d4d7
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AbstractPerformativeInjectAction.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Abstract base used by inject actions of AMQP Performatives
+ *
+ * @param <P> the AMQP performative being sent.
+ */
+public abstract class AbstractPerformativeInjectAction<P extends DescribedType> implements ScriptedAction {
+
+    public static final int CHANNEL_UNSET = -1;
+
+    private final AMQPTestDriver driver;
+
+    private int channel = CHANNEL_UNSET;
+    private int delay = -1;
+
+    public AbstractPerformativeInjectAction(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    @Override
+    public final AbstractPerformativeInjectAction<P> now() {
+        // Give actors a chance to prepare.
+        beforeActionPerformed(driver);
+        driver.sendAMQPFrame(onChannel(), getPerformative(), getPayload());
+        return this;
+    }
+
+    @Override
+    public final AbstractPerformativeInjectAction<P> later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public final AbstractPerformativeInjectAction<P> queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+
+    @Override
+    public final AbstractPerformativeInjectAction<P> perform(AMQPTestDriver driver) {
+        if (afterDelay() > 0) {
+            driver.afterDelay(afterDelay(), new ScriptedAction() {
+
+                @Override
+                public ScriptedAction queue() {
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction perform(AMQPTestDriver driver) {
+                    return AbstractPerformativeInjectAction.this.now();
+                }
+
+                @Override
+                public ScriptedAction now() {
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction later(int waitTime) {
+                    return this;
+                }
+
+                @Override
+                public String toString() {
+                    return AbstractPerformativeInjectAction.this.toString();
+                }
+            });
+        } else {
+            return now();
+        }
+
+        return this;
+    }
+
+    public int onChannel() {
+        return channel;
+    }
+
+    public int afterDelay() {
+        return delay;
+    }
+
+    public AbstractPerformativeInjectAction<?> afterDelay(int delay) {
+        this.delay = delay;
+        return this;
+    }
+
+    public AbstractPerformativeInjectAction<?> onChannel(int channel) {
+        this.channel = channel;
+        return this;
+    }
+
+    public AbstractPerformativeInjectAction<?> onChannel(UnsignedShort channel) {
+        this.channel = channel.intValue();
+        return this;
+    }
+
+    /**
+     * @return the AMQP Performative that is to be sent as a result of this action.
+     */
+    public abstract P getPerformative();
+
+    /**
+     * @return the buffer containing the payload that should be sent as part of this action.
+     */
+    public ByteBuf getPayload() {
+        return null;
+    }
+
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // Subclass can override to modify driver of update performative state.
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AbstractSaslPerformativeInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AbstractSaslPerformativeInjectAction.java
new file mode 100644
index 0000000..0bb9dc6
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AbstractSaslPerformativeInjectAction.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+/**
+ * Abstract base used by inject actions of SASL Performatives
+ *
+ * @param <P> the SASL performative being sent.
+ */
+public abstract class AbstractSaslPerformativeInjectAction<P extends DescribedType> implements ScriptedAction {
+
+    public static final int CHANNEL_UNSET = -1;
+
+    private final AMQPTestDriver driver;
+
+    private int channel = CHANNEL_UNSET;
+
+    public AbstractSaslPerformativeInjectAction(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    @Override
+    public AbstractSaslPerformativeInjectAction<P> now() {
+        perform(driver);
+        return this;
+    }
+
+    @Override
+    public AbstractSaslPerformativeInjectAction<P> later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public AbstractSaslPerformativeInjectAction<P> queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+
+    @Override
+    public AbstractSaslPerformativeInjectAction<P> perform(AMQPTestDriver driver) {
+        driver.sendSaslFrame(onChannel(), getPerformative());
+        return this;
+    }
+
+    public int onChannel() {
+        return this.channel;
+    }
+
+    public AbstractSaslPerformativeInjectAction<?> onChannel(int channel) {
+        this.channel = channel;
+        return this;
+    }
+
+    public abstract P getPerformative();
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AttachInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AttachInjectAction.java
new file mode 100644
index 0000000..4da8680
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/AttachInjectAction.java
@@ -0,0 +1,540 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Outcome;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.SenderSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+/**
+ * AMQP Attach injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class AttachInjectAction extends AbstractPerformativeInjectAction<Attach> {
+
+    private final Attach attach = new Attach();
+
+    private boolean explicitlyNullName;
+    private boolean explicitlyNullHandle;
+    private boolean nullSourceRequired;
+    private boolean nullTargetRequired;
+
+    public AttachInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Attach getPerformative() {
+        return attach;
+    }
+
+    public AttachInjectAction withName(String name) {
+        explicitlyNullName = name == null;
+        attach.setName(name);
+        return this;
+    }
+
+    public AttachInjectAction withHandle(int handle) {
+        attach.setHandle(UnsignedInteger.valueOf(handle));
+        return this;
+    }
+
+    public AttachInjectAction withHandle(long handle) {
+        attach.setHandle(UnsignedInteger.valueOf(handle));
+        return this;
+    }
+
+    public AttachInjectAction withHandle(UnsignedInteger handle) {
+        explicitlyNullHandle = handle == null;
+        attach.setHandle(handle);
+        return this;
+    }
+
+    public AttachInjectAction withRole(boolean role) {
+        attach.setRole(role);
+        return this;
+    }
+
+    public AttachInjectAction withRole(Role role) {
+        attach.setRole(role.getValue());
+        return this;
+    }
+
+    public AttachInjectAction ofSender() {
+        attach.setRole(Role.SENDER.getValue());
+        return this;
+    }
+
+    public AttachInjectAction ofReceiver() {
+        attach.setRole(Role.RECEIVER.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withSndSettleMode(byte sndSettleMode) {
+        attach.setSenderSettleMode(UnsignedByte.valueOf(sndSettleMode));
+        return this;
+    }
+
+    public AttachInjectAction withSndSettleMode(Byte sndSettleMode) {
+        attach.setSenderSettleMode(sndSettleMode == null ? null : UnsignedByte.valueOf(sndSettleMode.byteValue()));
+        return this;
+    }
+
+    public AttachInjectAction withSndSettleMode(SenderSettleMode sndSettleMode) {
+        attach.setSenderSettleMode(sndSettleMode == null ? null : sndSettleMode.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withSenderSettleModeMixed() {
+        attach.setSenderSettleMode(SenderSettleMode.MIXED.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withSenderSettleModeSettled() {
+        attach.setSenderSettleMode(SenderSettleMode.SETTLED.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withSenderSettleModeUnsettled() {
+        attach.setSenderSettleMode(SenderSettleMode.UNSETTLED.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withRcvSettleMode(byte rcvSettleMode) {
+        attach.setReceiverSettleMode(UnsignedByte.valueOf(rcvSettleMode));
+        return this;
+    }
+
+    public AttachInjectAction withRcvSettleMode(Byte rcvSettleMode) {
+        attach.setReceiverSettleMode(rcvSettleMode == null ? null : UnsignedByte.valueOf(rcvSettleMode.byteValue()));
+        return this;
+    }
+
+    public AttachInjectAction withRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        attach.setReceiverSettleMode(rcvSettleMode == null ? null : rcvSettleMode.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withReceivervSettlesFirst() {
+        attach.setReceiverSettleMode(ReceiverSettleMode.FIRST.getValue());
+        return this;
+    }
+
+    public AttachInjectAction withReceivervSettlesSecond() {
+        attach.setReceiverSettleMode(ReceiverSettleMode.SECOND.getValue());
+        return this;
+    }
+
+    public boolean isNullSourceRequired() {
+        return nullSourceRequired;
+    }
+
+    public AttachInjectAction withNullSource() {
+        nullSourceRequired = true;
+        attach.setSource(null);
+        return this;
+    }
+
+    public SourceBuilder withSource() {
+        nullSourceRequired = false;
+        return new SourceBuilder(getOrCreateSouce());
+    }
+
+    public AttachInjectAction withSource(Source source) {
+        nullSourceRequired = source == null;
+        attach.setSource(source);
+        return this;
+    }
+
+    public boolean isNullTargetRequired() {
+        return nullTargetRequired;
+    }
+
+    public AttachInjectAction withNullTarget() {
+        nullTargetRequired = true;
+        attach.setTarget((Target) null);
+        return this;
+    }
+
+    public TargetBuilder withTarget() {
+        nullSourceRequired = false;
+        return new TargetBuilder(getOrCreateTarget());
+    }
+
+    public CoordinatorBuilder withCoordinator() {
+        nullSourceRequired = false;
+        return new CoordinatorBuilder(getOrCreateCoordinator());
+    }
+
+    public AttachInjectAction withTarget(Target target) {
+        nullTargetRequired = target == null;
+        attach.setTarget(target);
+        return this;
+    }
+
+    public AttachInjectAction withTarget(Coordinator coordinator) {
+        nullTargetRequired = coordinator == null;
+        attach.setTarget(coordinator);
+        return this;
+    }
+
+    public AttachInjectAction withUnsettled(Map<Binary, DeliveryState> unsettled) {
+        if (unsettled != null) {
+            Map<Binary, DescribedType> converted = new LinkedHashMap<>();
+            for (Entry<Binary, DeliveryState> entry : unsettled.entrySet()) {
+                converted.put(entry.getKey(), entry.getValue());
+            }
+
+            attach.setUnsettled(converted);
+        }
+        return this;
+    }
+
+    public AttachInjectAction withIncompleteUnsettled(boolean incomplete) {
+        attach.setIncompleteUnsettled(incomplete);
+        return this;
+    }
+
+    public AttachInjectAction withInitialDeliveryCount(long initialDeliveryCount) {
+        attach.setInitialDeliveryCount(UnsignedInteger.valueOf(initialDeliveryCount));
+        return this;
+    }
+
+    public AttachInjectAction withMaxMessageSize(UnsignedLong maxMessageSize) {
+        attach.setMaxMessageSize(maxMessageSize);
+        return this;
+    }
+
+    public AttachInjectAction withOfferedCapabilities(String... offeredCapabilities) {
+        attach.setOfferedCapabilities(TypeMapper.toSymbolArray(offeredCapabilities));
+        return this;
+    }
+
+    public AttachInjectAction withOfferedCapabilities(Symbol... offeredCapabilities) {
+        attach.setOfferedCapabilities(offeredCapabilities);
+        return this;
+    }
+
+    public AttachInjectAction withDesiredCapabilities(String... desiredCapabilities) {
+        attach.setDesiredCapabilities(TypeMapper.toSymbolArray(desiredCapabilities));
+        return this;
+    }
+
+    public AttachInjectAction withDesiredCapabilities(Symbol... desiredCapabilities) {
+        attach.setDesiredCapabilities(desiredCapabilities);
+        return this;
+    }
+
+    public AttachInjectAction withPropertiesMap(Map<String, Object> properties) {
+        attach.setProperties(TypeMapper.toSymbolKeyedMap(properties));
+        return this;
+    }
+
+    public AttachInjectAction withProperties(Map<Symbol, Object> properties) {
+        attach.setProperties(properties);
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // A test that is trying to send an unsolicited attach must provide a channel as we
+        // won't attempt to make up one since we aren't sure what the intent here is.
+        if (onChannel() == CHANNEL_UNSET) {
+            if (driver.sessions().getLastLocallyOpenedSession() == null) {
+                throw new AssertionError("Scripted Action cannot run without a configured channel: " +
+                                         "No locally opened session exists to auto select a channel.");
+            }
+
+            onChannel(driver.sessions().getLastLocallyOpenedSession().getLocalChannel().intValue());
+        }
+
+        final UnsignedShort localChannel = UnsignedShort.valueOf(onChannel());
+        final SessionTracker session = driver.sessions().getSessionFromLocalChannel(localChannel);
+
+        // A test might be trying to send Attach outside of session scope to check for error handling
+        // of unexpected performatives so we just allow no session cases and send what we are told.
+        if (session != null) {
+            if (attach.getName() == null && !explicitlyNullName) {
+                attach.setName(UUID.randomUUID().toString());
+            }
+
+            if (attach.getHandle() == null && !explicitlyNullHandle) {
+                attach.setHandle(session.findFreeLocalHandle());
+            }
+
+            // Do not signal the session that we created a link if it carries an invalid null handle
+            // as that would trigger other exceptions, just pass it on as the test is likely trying
+            // to validate something specific.
+            if (attach.getHandle() != null) {
+                session.handleLocalAttach(attach);
+            }
+        } else {
+            if (attach.getHandle() == null && !explicitlyNullHandle) {
+                throw new AssertionError("Attach must carry a handle or have an explicity set null handle.");
+            }
+        }
+    }
+
+    private Source getOrCreateSouce() {
+        if (attach.getSource() == null) {
+            attach.setSource(new Source());
+        }
+        return attach.getSource();
+    }
+
+    private Target getOrCreateTarget() {
+        if (attach.getTarget() == null) {
+            attach.setTarget(new Target());
+        }
+        return (Target) attach.getTarget();
+    }
+
+    private Coordinator getOrCreateCoordinator() {
+        if (attach.getTarget() == null) {
+            attach.setTarget(new Coordinator());
+        }
+        return (Coordinator) attach.getTarget();
+    }
+
+    //----- Builders for Source and Target to make test writing simpler
+
+    protected abstract class TerminusBuilder {
+
+        public AttachInjectAction also() {
+            return AttachInjectAction.this;
+        }
+
+        public AttachInjectAction and() {
+            return AttachInjectAction.this;
+        }
+    }
+
+    public final class SourceBuilder extends TerminusBuilder {
+
+        private final Source source;
+
+        public SourceBuilder(Source source) {
+            this.source = source;
+        }
+
+        public SourceBuilder withAddress(String address) {
+            source.setAddress(address);
+            return this;
+        }
+
+        public SourceBuilder withDurability(TerminusDurability durability) {
+            source.setDurable(durability.getValue());
+            return this;
+        }
+
+        public SourceBuilder withExpiryPolicy(TerminusExpiryPolicy expriyPolicy) {
+            source.setExpiryPolicy(expriyPolicy.getPolicy());
+            return this;
+        }
+
+        public SourceBuilder withTimeout(int timeout) {
+            source.setTimeout(UnsignedInteger.valueOf(timeout));
+            return this;
+        }
+
+        public SourceBuilder withTimeout(long timeout) {
+            source.setTimeout(UnsignedInteger.valueOf(timeout));
+            return this;
+        }
+
+        public SourceBuilder withTimeout(UnsignedInteger timeout) {
+            source.setTimeout(timeout);
+            return this;
+        }
+
+        public SourceBuilder withDynamic(boolean dynamic) {
+            source.setDynamic(Boolean.valueOf(dynamic));
+            return this;
+        }
+
+        public SourceBuilder withDynamic(Boolean dynamic) {
+            source.setDynamic(dynamic);
+            return this;
+        }
+
+        public SourceBuilder withDynamicNodePropertiesMap(Map<Symbol, Object> properties) {
+            source.setDynamicNodeProperties(properties);
+            return this;
+        }
+
+        public SourceBuilder withDynamicNodeProperties(Map<String, Object> properties) {
+            source.setDynamicNodeProperties(TypeMapper.toSymbolKeyedMap(properties));
+            return this;
+        }
+
+        public SourceBuilder withDistributionMode(String mode) {
+            source.setDistributionMode(Symbol.valueOf(mode));
+            return this;
+        }
+
+        public SourceBuilder withDistributionMode(Symbol mode) {
+            source.setDistributionMode(mode);
+            return this;
+        }
+
+        public SourceBuilder withFilter(Map<Symbol, Object> filters) {
+            source.setFilter(filters);
+            return this;
+        }
+
+        public SourceBuilder withFilterMap(Map<String, Object> filters) {
+            source.setFilter(TypeMapper.toSymbolKeyedMap(filters));
+            return this;
+        }
+
+        public SourceBuilder withDefaultOutcome(Outcome outcome) {
+            source.setDefaultOutcome((DescribedType) outcome);
+            return this;
+        }
+
+        public SourceBuilder withOutcomes(Symbol... outcomes) {
+            source.setOutcomes(outcomes);
+            return this;
+        }
+
+        public SourceBuilder withOutcomes(String... outcomes) {
+            source.setOutcomes(outcomes);
+            return this;
+        }
+
+        public SourceBuilder withCapabilities(Symbol... capabilities) {
+            source.setCapabilities(capabilities);
+            return this;
+        }
+
+        public SourceBuilder withCapabilities(String... capabilities) {
+            source.setCapabilities(capabilities);
+            return this;
+        }
+    }
+
+    public final class TargetBuilder extends TerminusBuilder {
+
+        private final Target target;
+
+        public TargetBuilder(Target target) {
+            this.target = target;
+        }
+
+        public TargetBuilder withAddress(String address) {
+            target.setAddress(address);
+            return this;
+        }
+
+        public TargetBuilder withDurability(TerminusDurability durability) {
+            target.setDurable(durability.getValue());
+            return this;
+        }
+
+        public TargetBuilder withExpiryPolicy(TerminusExpiryPolicy expriyPolicy) {
+            target.setExpiryPolicy(expriyPolicy.getPolicy());
+            return this;
+        }
+
+        public TargetBuilder withTimeout(int timeout) {
+            target.setTimeout(UnsignedInteger.valueOf(timeout));
+            return this;
+        }
+
+        public TargetBuilder withTimeout(long timeout) {
+            target.setTimeout(UnsignedInteger.valueOf(timeout));
+            return this;
+        }
+
+        public TargetBuilder withTimeout(UnsignedInteger timeout) {
+            target.setTimeout(timeout);
+            return this;
+        }
+
+        public TargetBuilder withDynamic(boolean dynamic) {
+            target.setDynamic(Boolean.valueOf(dynamic));
+            return this;
+        }
+
+        public TargetBuilder withDynamic(Boolean dynamic) {
+            target.setDynamic(dynamic);
+            return this;
+        }
+
+        public TargetBuilder withDynamicNodePropertiesMap(Map<Symbol, Object> properties) {
+            target.setDynamicNodeProperties(properties);
+            return this;
+        }
+
+        public TargetBuilder withDynamicNodeProperties(Map<String, Object> properties) {
+            target.setDynamicNodeProperties(TypeMapper.toSymbolKeyedMap(properties));
+            return this;
+        }
+
+        public TargetBuilder withCapabilities(String... capabilities) {
+            target.setCapabilities(TypeMapper.toSymbolArray(capabilities));
+            return this;
+        }
+
+        public TargetBuilder withCapabilities(Symbol... capabilities) {
+            target.setCapabilities(capabilities);
+            return this;
+        }
+    }
+
+    public final class CoordinatorBuilder extends TerminusBuilder {
+
+        private final Coordinator coordinator;
+
+        public CoordinatorBuilder(Coordinator coordinator) {
+            this.coordinator = coordinator;
+        }
+
+        public CoordinatorBuilder withCapabilities(String... capabilities) {
+            coordinator.setCapabilities(TypeMapper.toSymbolArray(capabilities));
+            return this;
+        }
+
+        public CoordinatorBuilder withCapabilities(Symbol... capabilities) {
+            coordinator.setCapabilities(capabilities);
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/BeginInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/BeginInjectAction.java
new file mode 100644
index 0000000..5ef8c84
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/BeginInjectAction.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+/**
+ * AMQP Begin injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class BeginInjectAction extends AbstractPerformativeInjectAction<Begin> {
+
+    private static final UnsignedInteger DEFAULT_WINDOW_SIZE = UnsignedInteger.valueOf(Integer.MAX_VALUE);
+
+    private final Begin begin = new Begin();
+    {
+        begin.setNextOutgoingId(UnsignedInteger.ONE);
+        begin.setIncomingWindow(DEFAULT_WINDOW_SIZE);
+        begin.setOutgoingWindow(DEFAULT_WINDOW_SIZE);
+    }
+
+    /**
+     * Set defaults for the required fields of the performative
+     *
+     * @param driver
+     *      The test driver that will run this action.
+     */
+    public BeginInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Begin getPerformative() {
+        return begin;
+    }
+
+    public BeginInjectAction withRemoteChannel(int remoteChannel) {
+        begin.setRemoteChannel(UnsignedShort.valueOf((short) remoteChannel));
+        return this;
+    }
+
+    public BeginInjectAction withRemoteChannel(UnsignedShort remoteChannel) {
+        begin.setRemoteChannel(remoteChannel);
+        return this;
+    }
+
+    public BeginInjectAction withNextOutgoingId(int nextOutgoingId) {
+        begin.setNextOutgoingId(UnsignedInteger.valueOf(nextOutgoingId));
+        return this;
+    }
+
+    public BeginInjectAction withNextOutgoingId(long nextOutgoingId) {
+        begin.setNextOutgoingId(UnsignedInteger.valueOf(nextOutgoingId));
+        return this;
+    }
+
+    public BeginInjectAction withNextOutgoingId(UnsignedInteger nextOutgoingId) {
+        begin.setNextOutgoingId(nextOutgoingId);
+        return this;
+    }
+
+    public BeginInjectAction withIncomingWindow(int incomingWindow) {
+        begin.setIncomingWindow(UnsignedInteger.valueOf(incomingWindow));
+        return this;
+    }
+
+    public BeginInjectAction withIncomingWindow(long incomingWindow) {
+        begin.setIncomingWindow(UnsignedInteger.valueOf(incomingWindow));
+        return this;
+    }
+
+    public BeginInjectAction withIncomingWindow(UnsignedInteger incomingWindow) {
+        begin.setIncomingWindow(incomingWindow);
+        return this;
+    }
+
+    public BeginInjectAction withOutgoingWindow(int outgoingWindow) {
+        begin.setOutgoingWindow(UnsignedInteger.valueOf(outgoingWindow));
+        return this;
+    }
+
+    public BeginInjectAction withOutgoingWindow(long outgoingWindow) {
+        begin.setOutgoingWindow(UnsignedInteger.valueOf(outgoingWindow));
+        return this;
+    }
+
+    public BeginInjectAction withOutgoingWindow(UnsignedInteger outgoingWindow) {
+        begin.setOutgoingWindow(outgoingWindow);
+        return this;
+    }
+
+    public BeginInjectAction withHandleMax(int handleMax) {
+        begin.setHandleMax(UnsignedInteger.valueOf(handleMax));
+        return this;
+    }
+
+    public BeginInjectAction withHandleMax(long handleMax) {
+        begin.setHandleMax(UnsignedInteger.valueOf(handleMax));
+        return this;
+    }
+
+    public BeginInjectAction withHandleMax(UnsignedInteger handleMax) {
+        begin.setHandleMax(handleMax);
+        return this;
+    }
+
+    public BeginInjectAction withOfferedCapabilities(String... offeredCapabilities) {
+        begin.setOfferedCapabilities(TypeMapper.toSymbolArray(offeredCapabilities));
+        return this;
+    }
+
+    public BeginInjectAction withOfferedCapabilities(Symbol... offeredCapabilities) {
+        begin.setOfferedCapabilities(offeredCapabilities);
+        return this;
+    }
+
+    public BeginInjectAction withDesiredCapabilities(String... desiredCapabilities) {
+        begin.setDesiredCapabilities(TypeMapper.toSymbolArray(desiredCapabilities));
+        return this;
+    }
+
+    public BeginInjectAction withDesiredCapabilities(Symbol... desiredCapabilities) {
+        begin.setDesiredCapabilities(desiredCapabilities);
+        return this;
+    }
+
+    public BeginInjectAction withProperties(Map<String, Object> properties) {
+        begin.setProperties(TypeMapper.toSymbolKeyedMap(properties));
+        return this;
+    }
+
+    public BeginInjectAction withPropertiesMap(Map<Symbol, Object> properties) {
+        begin.setProperties(properties);
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // We fill in a channel using the next available channel id if one isn't set, then
+        // report the outbound begin to the session so it can track this new session.
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(driver.sessions().findFreeLocalChannel());
+        }
+
+        driver.sessions().handleLocalBegin(begin, UnsignedShort.valueOf(onChannel()));
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ByteBufferInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ByteBufferInjectAction.java
new file mode 100644
index 0000000..11db9b2
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ByteBufferInjectAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted action that will write the contents of a given buffer out through the driver.
+ */
+public class ByteBufferInjectAction implements ScriptedAction {
+
+    private final ByteBuf buffer;
+    private final AMQPTestDriver driver;
+
+    public ByteBufferInjectAction(AMQPTestDriver driver, ByteBuf buffer) {
+        this.buffer = buffer;
+        this.driver = driver;
+    }
+
+    @Override
+    public ByteBufferInjectAction perform(AMQPTestDriver driver) {
+        driver.sendBytes(buffer);
+        return this;
+    }
+
+    @Override
+    public ByteBufferInjectAction now() {
+        perform(driver);
+        return this;
+    }
+
+    @Override
+    public ByteBufferInjectAction later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public ByteBufferInjectAction queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/CloseInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/CloseInjectAction.java
new file mode 100644
index 0000000..e476de4
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/CloseInjectAction.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Close;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+/**
+ * AMQP Close injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class CloseInjectAction extends AbstractPerformativeInjectAction<Close> {
+
+    private final Close close = new Close();
+
+    public CloseInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Close getPerformative() {
+        return close;
+    }
+
+    public CloseInjectAction withErrorCondition(ErrorCondition error) {
+        close.setError(error);
+        return this;
+    }
+
+    public CloseInjectAction withErrorCondition(String condition, String description) {
+        close.setError(new ErrorCondition(Symbol.valueOf(condition), description));
+        return this;
+    }
+
+    public CloseInjectAction withErrorCondition(Symbol condition, String description) {
+        close.setError(new ErrorCondition(condition, description));
+        return this;
+    }
+
+    public CloseInjectAction withErrorCondition(String condition, String description, Map<String, Object> info) {
+        close.setError(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info)));
+        return this;
+    }
+
+    public CloseInjectAction withErrorCondition(Symbol condition, String description, Map<Symbol, Object> info) {
+        close.setError(new ErrorCondition(condition, description, info));
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // We fill in a channel using the next available channel id if one isn't set, then
+        // report the outbound begin to the session so it can track this new session.
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(0);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ConnectionDropAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ConnectionDropAction.java
new file mode 100644
index 0000000..07686a0
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ConnectionDropAction.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ProtonTestPeer;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+
+/**
+ * Action that drops the netty connection to the remote once invoked.
+ */
+public class ConnectionDropAction implements ScriptedAction {
+
+    private final ProtonTestPeer peer;
+    private int delay = -1;
+
+    public ConnectionDropAction(ProtonTestPeer peer) {
+        this.peer = peer;
+    }
+
+    @Override
+    public ScriptedAction now() {
+        peer.close();
+        return this;
+    }
+
+    @Override
+    public ScriptedAction later(int waitTime) {
+        peer.getDriver().afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public ScriptedAction queue() {
+        peer.getDriver().addScriptedElement(this);
+        return this;
+    }
+
+    @Override
+    public ScriptedAction perform(AMQPTestDriver driver) {
+        if (afterDelay() > 0) {
+            driver.afterDelay(afterDelay(), new ScriptedAction() {
+
+                @Override
+                public ScriptedAction queue() {
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction perform(AMQPTestDriver driver) {
+                    return ConnectionDropAction.this.now();
+                }
+
+                @Override
+                public ScriptedAction now() {
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction later(int waitTime) {
+                    return this;
+                }
+            });
+        } else {
+            now();
+        }
+
+        return this;
+    }
+
+    public int afterDelay() {
+        return delay;
+    }
+
+    public ConnectionDropAction afterDelay(int delay) {
+        this.delay = delay;
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DeclareInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DeclareInjectAction.java
new file mode 100644
index 0000000..d7e8d0b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DeclareInjectAction.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declare;
+
+/**
+ * Inject a Discharge performative encoded as the payload of a Transfer Frame.
+ */
+public class DeclareInjectAction extends TransferInjectAction {
+
+    public DeclareInjectAction(AMQPTestDriver driver) {
+        super(driver);
+
+        withBody().withDescribed(new Declare());
+    }
+
+    public DeclareInjectAction withDeclare(Declare declare) {
+        withBody().withDescribed(declare);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DetachInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DetachInjectAction.java
new file mode 100644
index 0000000..83b387f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DetachInjectAction.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+/**
+ * AMQP Detach injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class DetachInjectAction extends AbstractPerformativeInjectAction<Detach> {
+
+    private final Detach detach = new Detach();
+
+    public DetachInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Detach getPerformative() {
+        return detach;
+    }
+
+    public DetachInjectAction withHandle(int handle) {
+        detach.setHandle(UnsignedInteger.valueOf(handle));
+        return this;
+    }
+
+    public DetachInjectAction withHandle(long handle) {
+        detach.setHandle(UnsignedInteger.valueOf(handle));
+        return this;
+    }
+
+    public DetachInjectAction withHandle(UnsignedInteger handle) {
+        detach.setHandle(handle);
+        return this;
+    }
+
+    public DetachInjectAction withClosed(boolean closed) {
+        detach.setClosed(closed);
+        return this;
+    }
+
+    public DetachInjectAction withClosed(Boolean closed) {
+        detach.setClosed(closed);
+        return this;
+    }
+
+    public DetachInjectAction withErrorCondition(ErrorCondition error) {
+        detach.setError(error);
+        return this;
+    }
+
+    public DetachInjectAction withErrorCondition(String condition, String description) {
+        detach.setError(new ErrorCondition(Symbol.valueOf(condition), description));
+        return this;
+    }
+
+    public DetachInjectAction withErrorCondition(Symbol condition, String description) {
+        detach.setError(new ErrorCondition(condition, description));
+        return this;
+    }
+
+    public DetachInjectAction withErrorCondition(String condition, String description, Map<String, Object> info) {
+        detach.setError(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info)));
+        return this;
+    }
+
+    public DetachInjectAction withErrorCondition(Symbol condition, String description, Map<Symbol, Object> info) {
+        detach.setError(new ErrorCondition(condition, description, info));
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // A test that is trying to send an unsolicited detach must provide a channel as we
+        // won't attempt to make up one since we aren't sure what the intent here is.
+        if (onChannel() == CHANNEL_UNSET) {
+            if (driver.sessions().getLastLocallyOpenedSession() == null) {
+                throw new AssertionError("Scripted Action cannot run without a configured channel: " +
+                                         "No locally opened session exists to auto select a channel.");
+            }
+
+            onChannel(driver.sessions().getLastLocallyOpenedSession().getLocalChannel().intValue());
+        }
+
+        final UnsignedShort localChannel = UnsignedShort.valueOf(onChannel());
+        final SessionTracker session = driver.sessions().getSessionFromLocalChannel(localChannel);
+
+        // A test might be trying to send Attach outside of session scope to check for error handling
+        // of unexpected performatives so we just allow no session cases and send what we are told.
+        if (session != null) {
+            // Auto select last opened sender on last opened session.  Later an option could
+            // be added to allow forcing the handle to be null for testing specification requirements.
+            if (detach.getHandle() == null) {
+                detach.setHandle(session.getLastOpenedLink().getHandle());
+            }
+
+            session.handleLocalDetach(detach);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DetachLastCoordinatorInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DetachLastCoordinatorInjectAction.java
new file mode 100644
index 0000000..aef2756
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DetachLastCoordinatorInjectAction.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.LinkTracker;
+
+/**
+ * Detach actions that ignores any other handle or channel configuration
+ * and specifically targets the most recently created Coordinator Link.
+ */
+public class DetachLastCoordinatorInjectAction extends DetachInjectAction {
+
+    /**
+     * @param driver
+     */
+    public DetachLastCoordinatorInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        LinkTracker tracker = driver.sessions().getLastOpenedCoordinator();
+
+        onChannel(tracker.getSession().getLocalChannel().intValue());
+        getPerformative().setHandle(tracker.getHandle());
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DischargeInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DischargeInjectAction.java
new file mode 100644
index 0000000..558a56c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DischargeInjectAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Discharge;
+
+/**
+ * Inject a Transaction Discharge frame.
+ */
+public class DischargeInjectAction extends TransferInjectAction {
+
+    private Discharge discharge = new Discharge();
+
+    public DischargeInjectAction(AMQPTestDriver driver) {
+        super(driver);
+
+        withBody().withDescribed(discharge);
+    }
+
+    public DischargeInjectAction withFail(boolean fail) {
+        discharge.setFail(fail);
+        return this;
+    }
+
+    public DischargeInjectAction withTxnId(byte[] txnId) {
+        discharge.setTxnId(new Binary(txnId));
+        return this;
+    }
+
+    public DischargeInjectAction withTxnId(Binary txnId) {
+        discharge.setTxnId(txnId);
+        return this;
+    }
+
+    public DischargeInjectAction withDischarge(Discharge discharge) {
+        withBody().withDescribed(discharge);
+        return this;
+    }
+
+    @Override
+    public DischargeInjectAction withHandle(long handle) {
+        return (DischargeInjectAction) super.withHandle(handle);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DispositionInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DispositionInjectAction.java
new file mode 100644
index 0000000..578a341
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/DispositionInjectAction.java
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Rejected;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.TransactionalState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+
+/**
+ * AMQP Disposition injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class DispositionInjectAction extends AbstractPerformativeInjectAction<Disposition> {
+
+    private final Disposition disposition = new Disposition();
+    private final DeliveryStateBuilder stateBuilder = new DeliveryStateBuilder();
+
+    public DispositionInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Disposition getPerformative() {
+        return disposition;
+    }
+
+    public DispositionInjectAction withRole(boolean role) {
+        disposition.setRole(role);
+        return this;
+    }
+
+    public DispositionInjectAction withRole(Boolean role) {
+        disposition.setRole(role);
+        return this;
+    }
+
+    public DispositionInjectAction withRole(Role role) {
+        disposition.setRole(role.getValue());
+        return this;
+    }
+
+    public DispositionInjectAction withFirst(int first) {
+        disposition.setFirst(UnsignedInteger.valueOf(first));
+        return this;
+    }
+
+    public DispositionInjectAction withFirst(long first) {
+        disposition.setFirst(UnsignedInteger.valueOf(first));
+        return this;
+    }
+
+    public DispositionInjectAction withFirst(UnsignedInteger first) {
+        disposition.setFirst(first);
+        return this;
+    }
+
+    public DispositionInjectAction withLast(int last) {
+        disposition.setLast(UnsignedInteger.valueOf(last));
+        return this;
+    }
+
+    public DispositionInjectAction withLast(long last) {
+        disposition.setLast(UnsignedInteger.valueOf(last));
+        return this;
+    }
+
+    public DispositionInjectAction withLast(UnsignedInteger last) {
+        disposition.setLast(last);
+        return this;
+    }
+
+    public DispositionInjectAction withSettled(boolean settled) {
+        disposition.setSettled(settled);
+        return this;
+    }
+
+    public DeliveryStateBuilder withState() {
+        return stateBuilder;
+    }
+
+    public DispositionInjectAction withState(DeliveryState state) {
+        disposition.setState(state);
+        return this;
+    }
+
+    public DispositionInjectAction withBatchable(boolean batchable) {
+        disposition.setBatchable(batchable);
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // We fill in a channel using the next available channel id if one isn't set, then
+        // report the outbound begin to the session so it can track this new session.
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(driver.sessions().getLastLocallyOpenedSession().getLocalChannel().intValue());
+        }
+
+        // TODO - Process disposition in the local side of the link when needed for added validation
+    }
+
+    public final class DeliveryStateBuilder {
+
+        public DispositionInjectAction accepted() {
+            withState(Accepted.getInstance());
+            return DispositionInjectAction.this;
+        }
+
+        public DispositionInjectAction released() {
+            withState(Released.getInstance());
+            return DispositionInjectAction.this;
+        }
+
+        public DispositionInjectAction rejected() {
+            withState(new Rejected());
+            return DispositionInjectAction.this;
+        }
+
+        public DispositionInjectAction rejected(String condition, String description) {
+            withState(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return DispositionInjectAction.this;
+        }
+
+        public DispositionInjectAction modified() {
+            withState(new Modified());
+            return DispositionInjectAction.this;
+        }
+
+        public DispositionInjectAction modified(boolean failed) {
+            withState(new Modified().setDeliveryFailed(failed));
+            return DispositionInjectAction.this;
+        }
+
+        public DispositionInjectAction modified(boolean failed, boolean undeliverableHere) {
+            withState(new Modified().setDeliveryFailed(failed).setUndeliverableHere(undeliverableHere));
+            return DispositionInjectAction.this;
+        }
+
+        public TransactionalStateBuilder transactional() {
+            TransactionalStateBuilder builder = new TransactionalStateBuilder(DispositionInjectAction.this);
+            withState(builder.getState());
+            return builder;
+        }
+    }
+
+    //----- Provide a complex builder for Transactional DeliveryState
+
+    public static class TransactionalStateBuilder {
+
+        private final DispositionInjectAction action;
+        private final TransactionalState state = new TransactionalState();
+
+        public TransactionalStateBuilder(DispositionInjectAction action) {
+            this.action = action;
+        }
+
+        public TransactionalState getState() {
+            return state;
+        }
+
+        public DispositionInjectAction also() {
+            return action;
+        }
+
+        public DispositionInjectAction and() {
+            return action;
+        }
+
+        public TransactionalStateBuilder withTxnId(byte[] txnId) {
+            state.setTxnId(new Binary(txnId));
+            return this;
+        }
+
+        public TransactionalStateBuilder withTxnId(Binary txnId) {
+            state.setTxnId(txnId);
+            return this;
+        }
+
+        public TransactionalStateBuilder withOutcome(DeliveryState outcome) {
+            state.setOutcome(outcome);
+            return this;
+        }
+
+        // ----- Add a layer to allow configuring the outcome without specific type dependencies
+
+        public TransactionalStateBuilder withAccepted() {
+            withOutcome(Accepted.getInstance());
+            return this;
+        }
+
+        public TransactionalStateBuilder withReleased() {
+            withOutcome(Released.getInstance());
+            return this;
+        }
+
+        public TransactionalStateBuilder withRejected() {
+            withOutcome(new Rejected());
+            return this;
+        }
+
+        public TransactionalStateBuilder withRejected(String condition, String description) {
+            withOutcome(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return this;
+        }
+
+        public TransactionalStateBuilder withModified() {
+            withOutcome(new Modified());
+            return this;
+        }
+
+        public TransactionalStateBuilder withModified(boolean failed) {
+            withOutcome(new Modified().setDeliveryFailed(failed));
+            return this;
+        }
+
+        public TransactionalStateBuilder withModified(boolean failed, boolean undeliverableHere) {
+            withOutcome(new Modified().setDeliveryFailed(failed).setUndeliverableHere(undeliverableHere));
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/EmptyFrameInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/EmptyFrameInjectAction.java
new file mode 100644
index 0000000..c371060
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/EmptyFrameInjectAction.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+
+/**
+ * AMQP Empty Frame injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class EmptyFrameInjectAction implements ScriptedAction {
+
+    public static final int CHANNEL_UNSET = -1;
+
+    private int channel = CHANNEL_UNSET;
+
+    private final AMQPTestDriver driver;
+
+    public EmptyFrameInjectAction(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    public int onChannel() {
+        return this.channel;
+    }
+
+    @Override
+    public EmptyFrameInjectAction perform(AMQPTestDriver driver) {
+        driver.sendEmptyFrame(this.channel == CHANNEL_UNSET ? 0 : this.channel);
+        return this;
+    }
+
+    @Override
+    public EmptyFrameInjectAction now() {
+        perform(driver);
+        return this;
+    }
+
+    @Override
+    public EmptyFrameInjectAction later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public EmptyFrameInjectAction queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/EndInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/EndInjectAction.java
new file mode 100644
index 0000000..3bf6531
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/EndInjectAction.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+/**
+ * AMQP End injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class EndInjectAction extends AbstractPerformativeInjectAction<End> {
+
+    private final End end = new End();
+
+    public EndInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public End getPerformative() {
+        return end;
+    }
+
+    public EndInjectAction withErrorCondition(ErrorCondition error) {
+        end.setError(error);
+        return this;
+    }
+
+    public EndInjectAction withErrorCondition(String condition, String description) {
+        end.setError(new ErrorCondition(Symbol.valueOf(condition), description));
+        return this;
+    }
+
+    public EndInjectAction withErrorCondition(Symbol condition, String description) {
+        end.setError(new ErrorCondition(condition, description));
+        return this;
+    }
+
+    public EndInjectAction withErrorCondition(String condition, String description, Map<String, Object> info) {
+        end.setError(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info)));
+        return this;
+    }
+
+    public EndInjectAction withErrorCondition(Symbol condition, String description, Map<Symbol, Object> info) {
+        end.setError(new ErrorCondition(condition, description, info));
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // We fill in a channel using the next available channel id if one isn't set, then
+        // report the outbound begin to the session so it can track this new session.
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(driver.sessions().getLastLocallyOpenedSession().getLocalChannel().intValue());
+        }
+
+        driver.sessions().handleLocalEnd(end, UnsignedShort.valueOf(onChannel()));
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ExecuteUserCodeAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ExecuteUserCodeAction.java
new file mode 100644
index 0000000..7731577
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ExecuteUserCodeAction.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Objects;
+import java.util.concurrent.ForkJoinPool;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+
+/**
+ * Runs the given user code either now, later or after other test expectations have occurred.
+ *
+ * The given action will be executed on the common ForkJoinPool to prevent any blocking
+ * operations from affecting the test driver event loop itself.
+ */
+public class ExecuteUserCodeAction implements ScriptedAction {
+
+    private final AMQPTestDriver driver;
+    private final Runnable action;
+
+    private int delay = -1;
+
+    public ExecuteUserCodeAction(AMQPTestDriver driver, Runnable action) {
+        Objects.requireNonNull(driver, "Test Driver to use cannot be null");
+        Objects.requireNonNull(action, "Action to run cannot be null");
+
+        this.driver = driver;
+        this.action = action;
+    }
+
+    public int afterDelay() {
+        return delay;
+    }
+
+    public ExecuteUserCodeAction afterDelay(int delay) {
+        this.delay = delay;
+        return this;
+    }
+
+    @Override
+    public ExecuteUserCodeAction now() {
+        ForkJoinPool.commonPool().execute(action);
+        return this;
+    }
+
+    @Override
+    public ExecuteUserCodeAction later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public ExecuteUserCodeAction queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+
+    @Override
+    public ExecuteUserCodeAction perform(AMQPTestDriver driver) {
+        if (afterDelay() > 0) {
+            driver.afterDelay(afterDelay(), new ScriptedAction() {
+
+                @Override
+                public ScriptedAction queue() {
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction perform(AMQPTestDriver driver) {
+                    now();
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction now() {
+                    return this;
+                }
+
+                @Override
+                public ScriptedAction later(int waitTime) {
+                    return this;
+                }
+            });
+        } else {
+            now();
+        }
+
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/FlowInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/FlowInjectAction.java
new file mode 100644
index 0000000..7a15207
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/FlowInjectAction.java
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.LinkTracker;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+
+/**
+ * AMQP Flow injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class FlowInjectAction extends AbstractPerformativeInjectAction<Flow> {
+
+    private final Flow flow = new Flow();
+
+    private boolean explicitlyNullHandle;
+
+    public FlowInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Flow getPerformative() {
+        return flow;
+    }
+
+    public FlowInjectAction withNextIncomingId(long nextIncomingId) {
+        flow.setNextIncomingId(UnsignedInteger.valueOf(nextIncomingId));
+        return this;
+    }
+
+    public FlowInjectAction withNextIncomingId(UnsignedInteger nextIncomingId) {
+        flow.setNextIncomingId(nextIncomingId);
+        return this;
+    }
+
+    public FlowInjectAction withIncomingWindow(long incomingWindow) {
+        flow.setIncomingWindow(UnsignedInteger.valueOf(incomingWindow));
+        return this;
+    }
+
+    public FlowInjectAction withIncomingWindow(UnsignedInteger incomingWindow) {
+        flow.setIncomingWindow(incomingWindow);
+        return this;
+    }
+
+    public FlowInjectAction withNextOutgoingId(long nextOutgoingId) {
+        flow.setNextOutgoingId(UnsignedInteger.valueOf(nextOutgoingId));
+        return this;
+    }
+
+    public FlowInjectAction withNextOutgoingId(UnsignedInteger nextOutgoingId) {
+        flow.setNextOutgoingId(nextOutgoingId);
+        return this;
+    }
+
+    public FlowInjectAction withOutgoingWindow(long outgoingWindow) {
+        flow.setOutgoingWindow(UnsignedInteger.valueOf(outgoingWindow));
+        return this;
+    }
+
+    public FlowInjectAction withOutgoingWindow(UnsignedInteger outgoingWindow) {
+        flow.setOutgoingWindow(outgoingWindow);
+        return this;
+    }
+
+    public FlowInjectAction withHandle(long handle) {
+        flow.setHandle(UnsignedInteger.valueOf(handle));
+        return this;
+    }
+
+    public FlowInjectAction withHandle(UnsignedInteger handle) {
+        explicitlyNullHandle = handle == null;
+        flow.setHandle(handle);
+        return this;
+    }
+
+    public FlowInjectAction withNullHandle() {
+        explicitlyNullHandle = true;
+        flow.setHandle(null);
+        return this;
+    }
+
+    public FlowInjectAction withDeliveryCount(long deliveryCount) {
+        flow.setDeliveryCount(UnsignedInteger.valueOf(deliveryCount));
+        return this;
+    }
+
+    public FlowInjectAction withDeliveryCount(UnsignedInteger deliveryCount) {
+        flow.setDeliveryCount(deliveryCount);
+        return this;
+    }
+
+    public FlowInjectAction withLinkCredit(long linkCredit) {
+        flow.setLinkCredit(UnsignedInteger.valueOf(linkCredit));
+        return this;
+    }
+
+    public FlowInjectAction withLinkCredit(UnsignedInteger linkCredit) {
+        flow.setLinkCredit(linkCredit);
+        return this;
+    }
+
+    public FlowInjectAction withAvailable(long available) {
+        flow.setAvailable(UnsignedInteger.valueOf(available));
+        return this;
+    }
+
+    public FlowInjectAction withAvailable(UnsignedInteger available) {
+        flow.setAvailable(available);
+        return this;
+    }
+
+    public FlowInjectAction withDrain(boolean drain) {
+        flow.setDrain(drain);
+        return this;
+    }
+
+    public FlowInjectAction withEcho(boolean echo) {
+        flow.setEcho(echo);
+        return this;
+    }
+
+    public FlowInjectAction withProperties(Map<Symbol, Object> properties) {
+        flow.setProperties(properties);
+        return this;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        final SessionTracker session = driver.sessions().getLastLocallyOpenedSession();
+        final LinkTracker link = session.getLastOpenedLink();
+
+        // We fill in a channel using the next available channel id if one isn't set, then
+        // report the outbound begin to the session so it can track this new session.
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(session.getLocalChannel().intValue());
+        }
+
+        // Auto select last opened sender on last opened session, unless there's no links opened
+        // in which case we can assume this is session only flow.  Also check if the test scripted
+        // this as null which indicates the test is trying to send session only.
+        if (flow.getHandle() == null && !explicitlyNullHandle && link != null) {
+            flow.setHandle(link.getHandle());
+        }
+        if (flow.getIncomingWindow() == null) {
+            flow.setIncomingWindow(session.getLocalBegin().getIncomingWindow());
+        }
+        if (flow.getNextIncomingId() == null) {
+            flow.setNextIncomingId(session.getNextIncomingId());
+        }
+        if (flow.getNextOutgoingId() == null) {
+            flow.setNextOutgoingId(session.getLocalBegin().getNextOutgoingId());
+        }
+        if (flow.getOutgoingWindow() == null) {
+            flow.setOutgoingWindow(session.getLocalBegin().getOutgoingWindow());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/OpenInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/OpenInjectAction.java
new file mode 100644
index 0000000..5d46e8c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/OpenInjectAction.java
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+/**
+ * AMQP Open injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class OpenInjectAction extends AbstractPerformativeInjectAction<Open> {
+
+    private final Open open = new Open();
+
+    public OpenInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Open getPerformative() {
+        return open;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        if (getPerformative().getContainerId() == null) {
+            getPerformative().setContainerId("driver");
+        }
+
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(0);
+        }
+    }
+
+    public OpenInjectAction withContainerId(String containerId) {
+        open.setContainerId(containerId);
+        return this;
+    }
+
+    public OpenInjectAction withHostname(String hostname) {
+        open.setHostname(hostname);
+        return this;
+    }
+
+    public OpenInjectAction withMaxFrameSize(int maxFrameSize) {
+        open.setMaxFrameSize(UnsignedInteger.valueOf(maxFrameSize));
+        return this;
+    }
+
+    public OpenInjectAction withMaxFrameSize(long maxFrameSize) {
+        open.setMaxFrameSize(UnsignedInteger.valueOf(maxFrameSize));
+        return this;
+    }
+
+    public OpenInjectAction withMaxFrameSize(UnsignedInteger maxFrameSize) {
+        open.setMaxFrameSize(maxFrameSize);
+        return this;
+    }
+
+    public OpenInjectAction withChannelMax(int channelMax) {
+        open.setChannelMax(UnsignedShort.valueOf(channelMax));
+        return this;
+    }
+
+    public OpenInjectAction withChannelMax(short channelMax) {
+        open.setChannelMax(UnsignedShort.valueOf(channelMax));
+        return this;
+    }
+
+    public OpenInjectAction withChannelMax(UnsignedShort channelMax) {
+        open.setChannelMax(channelMax);
+        return this;
+    }
+
+    public OpenInjectAction withIdleTimeOut(int idleTimeout) {
+        open.setIdleTimeOut(UnsignedInteger.valueOf(idleTimeout));
+        return this;
+    }
+
+    public OpenInjectAction withIdleTimeOut(long idleTimeout) {
+        open.setIdleTimeOut(UnsignedInteger.valueOf(idleTimeout));
+        return this;
+    }
+
+    public OpenInjectAction withIdleTimeOut(UnsignedInteger idleTimeout) {
+        open.setIdleTimeOut(idleTimeout);
+        return this;
+    }
+
+    public OpenInjectAction withOutgoingLocales(String... outgoingLocales) {
+        open.setOutgoingLocales(TypeMapper.toSymbolArray(outgoingLocales));
+        return this;
+    }
+
+    public OpenInjectAction withOutgoingLocales(Symbol... outgoingLocales) {
+        open.setOutgoingLocales(outgoingLocales);
+        return this;
+    }
+
+    public OpenInjectAction withIncomingLocales(String... incomingLocales) {
+        open.setIncomingLocales(TypeMapper.toSymbolArray(incomingLocales));
+        return this;
+    }
+
+    public OpenInjectAction withIncomingLocales(Symbol... incomingLocales) {
+        open.setIncomingLocales(incomingLocales);
+        return this;
+    }
+
+    public OpenInjectAction withOfferedCapabilities(String... offeredCapabilities) {
+        open.setOfferedCapabilities(TypeMapper.toSymbolArray(offeredCapabilities));
+        return this;
+    }
+
+    public OpenInjectAction withOfferedCapabilities(Symbol... offeredCapabilities) {
+        open.setOfferedCapabilities(offeredCapabilities);
+        return this;
+    }
+
+    public OpenInjectAction withDesiredCapabilities(String... desiredCapabilities) {
+        open.setDesiredCapabilities(TypeMapper.toSymbolArray(desiredCapabilities));
+        return this;
+    }
+
+    public OpenInjectAction withDesiredCapabilities(Symbol... desiredCapabilities) {
+        open.setDesiredCapabilities(desiredCapabilities);
+        return this;
+    }
+
+    public OpenInjectAction withProperties(Map<String, Object> properties) {
+        open.setProperties(TypeMapper.toSymbolKeyedMap(properties));
+        return this;
+    }
+
+    public OpenInjectAction withPropertiesMap(Map<Symbol, Object> properties) {
+        open.setProperties(properties);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/RawBytesInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/RawBytesInjectAction.java
new file mode 100644
index 0000000..55e6cc6
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/RawBytesInjectAction.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * AMQP Empty Frame injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class RawBytesInjectAction implements ScriptedAction {
+
+    private final AMQPTestDriver driver;
+    private ByteBuf buffer;
+
+    public RawBytesInjectAction(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    @Override
+    public RawBytesInjectAction perform(AMQPTestDriver driver) {
+        if (buffer != null) {
+            driver.sendBytes(buffer);
+        }
+
+        return this;
+    }
+
+    @Override
+    public RawBytesInjectAction now() {
+        perform(driver);
+        return this;
+    }
+
+    @Override
+    public RawBytesInjectAction later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public RawBytesInjectAction queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+
+    public RawBytesInjectAction withBytes(byte[] bytes) {
+        this.buffer = Unpooled.wrappedBuffer(bytes);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslChallengeInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslChallengeInjectAction.java
new file mode 100644
index 0000000..82d524a
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslChallengeInjectAction.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslChallenge;
+
+/**
+ * AMQP SaslChallenge injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class SaslChallengeInjectAction extends AbstractSaslPerformativeInjectAction<SaslChallenge> {
+
+    private final SaslChallenge saslChallenge = new SaslChallenge();
+
+    public SaslChallengeInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public SaslChallengeInjectAction withChallenge(byte[] challenge) {
+        saslChallenge.setChallenge(new Binary(challenge));
+        return this;
+    }
+
+    public SaslChallengeInjectAction withChallenge(Binary challenge) {
+        saslChallenge.setChallenge(challenge);
+        return this;
+    }
+
+    @Override
+    public SaslChallenge getPerformative() {
+        return saslChallenge;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslInitInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslInitInjectAction.java
new file mode 100644
index 0000000..24ef11e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslInitInjectAction.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslInit;
+
+/**
+ * AMQP SaslInit injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class SaslInitInjectAction extends AbstractSaslPerformativeInjectAction<SaslInit> {
+
+    private final SaslInit saslInit = new SaslInit();
+
+    public SaslInitInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public SaslInitInjectAction withMechanism(String mechanism) {
+        saslInit.setMechanism(Symbol.valueOf(mechanism));
+        return this;
+    }
+
+    public SaslInitInjectAction withMechanism(Symbol mechanism) {
+        saslInit.setMechanism(mechanism);
+        return this;
+    }
+
+    public SaslInitInjectAction withMechanism(byte[] response) {
+        saslInit.setInitialResponse(new Binary(response));
+        return this;
+    }
+
+    public SaslInitInjectAction withMechanism(Binary response) {
+        saslInit.setInitialResponse(response);
+        return this;
+    }
+
+    public SaslInitInjectAction withHostname(String hostname) {
+        saslInit.setHostname(hostname);
+        return this;
+    }
+
+    @Override
+    public SaslInit getPerformative() {
+        return saslInit;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslMechanismsInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslMechanismsInjectAction.java
new file mode 100644
index 0000000..175c338
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslMechanismsInjectAction.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslMechanisms;
+
+/**
+ * AMQP SaslMechanisms injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class SaslMechanismsInjectAction extends AbstractSaslPerformativeInjectAction<SaslMechanisms> {
+
+    private final SaslMechanisms saslMechanisms = new SaslMechanisms();
+
+    public SaslMechanismsInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public SaslMechanismsInjectAction withMechanisms(String... saslServerMechanisms) {
+        Symbol[] mechanisms = null;
+
+        if (saslServerMechanisms != null) {
+            mechanisms = new Symbol[saslServerMechanisms.length];
+            for(int i = 0; i < saslServerMechanisms.length; ++i) {
+                mechanisms[i] = Symbol.valueOf(saslServerMechanisms[i]);
+            }
+        }
+
+        saslMechanisms.setSaslServerMechanisms(mechanisms);
+        return this;
+    }
+
+    public SaslMechanismsInjectAction withMechanisms(Symbol... saslServerMechanisms) {
+        saslMechanisms.setSaslServerMechanisms(saslServerMechanisms);
+        return this;
+    }
+
+    @Override
+    public SaslMechanisms getPerformative() {
+        return saslMechanisms;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslOutcomeInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslOutcomeInjectAction.java
new file mode 100644
index 0000000..c385f07
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslOutcomeInjectAction.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslCode;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+
+/**
+ * AMQP SaslOutcome injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class SaslOutcomeInjectAction extends AbstractSaslPerformativeInjectAction<SaslOutcome> {
+
+    private final SaslOutcome saslOutcome = new SaslOutcome();
+
+    public SaslOutcomeInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public SaslOutcomeInjectAction withCode(byte code) {
+        saslOutcome.setCode(UnsignedByte.valueOf(code));
+        return this;
+    }
+
+    public SaslOutcomeInjectAction withCode(SaslCode code) {
+        saslOutcome.setCode(code.getValue());
+        return this;
+    }
+
+    public SaslOutcomeInjectAction withAdditionalData(byte[] additionalData) {
+        saslOutcome.setAdditionalData(new Binary(additionalData));
+        return this;
+    }
+
+    public SaslOutcomeInjectAction withAdditionalData(Binary additionalData) {
+        saslOutcome.setAdditionalData(additionalData);
+        return this;
+    }
+
+    @Override
+    public SaslOutcome getPerformative() {
+        return saslOutcome;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslResponseInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslResponseInjectAction.java
new file mode 100644
index 0000000..ca5aa0c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/SaslResponseInjectAction.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslResponse;
+
+/**
+ * AMQP SaslResponse injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class SaslResponseInjectAction extends AbstractSaslPerformativeInjectAction<SaslResponse> {
+
+    private final SaslResponse saslResponse = new SaslResponse();
+
+    public SaslResponseInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public SaslResponseInjectAction withResponse(byte[] response) {
+        saslResponse.setResponse(new Binary(response));
+        return this;
+    }
+
+    public SaslResponseInjectAction withResponse(Binary response) {
+        saslResponse.setResponse(response);
+        return this;
+    }
+
+    @Override
+    public SaslResponse getPerformative() {
+        return saslResponse;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ScriptCompleteAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ScriptCompleteAction.java
new file mode 100644
index 0000000..49d5eec
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/ScriptCompleteAction.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedAction;
+
+/**
+ * Action that will signal when the script has completed all expectations and
+ * scripted actions or when a configured timeout occurs.
+ */
+public class ScriptCompleteAction implements ScriptedAction {
+
+    protected final AMQPTestDriver driver;
+    protected final CountDownLatch complete = new CountDownLatch(1);
+
+    public ScriptCompleteAction(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    @Override
+    public ScriptCompleteAction now() {
+        complete.countDown();
+        return this;
+    }
+
+    @Override
+    public ScriptCompleteAction queue() {
+        driver.addScriptedElement(this);
+        return this;
+    }
+
+    @Override
+    public ScriptCompleteAction later(int delay) {
+        driver.afterDelay(delay, this);
+        return this;
+    }
+
+    @Override
+    public ScriptCompleteAction perform(AMQPTestDriver driver) {
+        complete.countDown();
+        return this;
+    }
+
+    public void await() throws InterruptedException {
+        complete.await();
+    }
+
+    public void await(long timeout, TimeUnit units ) throws InterruptedException {
+        if (!complete.await(timeout, units)) {
+            throw new AssertionError("Timed out waiting for scripted expectations to be met", new TimeoutException());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/TransferInjectAction.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/TransferInjectAction.java
new file mode 100644
index 0000000..598c727
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/actions/TransferInjectAction.java
@@ -0,0 +1,595 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.actions;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpValue;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Data;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Footer;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Header;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Properties;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Rejected;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.TransactionalState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * AMQP Close injection action which can be added to a driver for write at a specific time or
+ * following on from some other action in the test script.
+ */
+public class TransferInjectAction extends AbstractPerformativeInjectAction<Transfer> {
+
+    private final Transfer transfer = new Transfer();
+    private final DeliveryStateBuilder stateBuilder = new DeliveryStateBuilder();
+
+    private ByteBuf payload;
+
+    private Header header;
+    private DeliveryAnnotations deliveryAnnotations;
+    private MessageAnnotations messageAnnotations;
+    private Properties properties;
+    private ApplicationProperties applicationProperties;
+    private DescribedType body;
+    private Footer footer;
+
+    public TransferInjectAction(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public Transfer getPerformative() {
+        return transfer;
+    }
+
+    @Override
+    public ByteBuf getPayload() {
+        if (payload == null) {
+            payload = encodePayload();
+        }
+        return payload;
+    }
+
+    @Override
+    protected void beforeActionPerformed(AMQPTestDriver driver) {
+        // We fill in a channel using the next available channel id if one isn't set, then
+        // report the outbound begin to the session so it can track this new session.
+        if (onChannel() == CHANNEL_UNSET) {
+            onChannel(driver.sessions().getLastLocallyOpenedSession().getLocalChannel().intValue());
+        }
+
+        // Auto select last opened receiver on last opened session.  Later an option could
+        // be added to allow forcing the handle to be null for testing specification requirements.
+        if (transfer.getHandle() == null) {
+            transfer.setHandle(driver.sessions().getLastLocallyOpenedSession().getLastOpenedRemoteReceiver().getHandle());
+        }
+
+        // Here we could check if the delivery Id is set and if not grab a valid
+        // next Id from the driver as well as checking for a session and using last
+        // created one if none set.
+    }
+
+    public TransferInjectAction withHandle(long handle) {
+        transfer.setHandle(UnsignedInteger.valueOf(handle));
+        return this;
+    }
+
+    public TransferInjectAction withDeliveryId(int deliveryId) {
+        transfer.setDeliveryId(UnsignedInteger.valueOf(deliveryId));
+        return this;
+    }
+
+    public TransferInjectAction withDeliveryId(long deliveryId) {
+        transfer.setDeliveryId(UnsignedInteger.valueOf(deliveryId));
+        return this;
+    }
+
+    public TransferInjectAction withDeliveryTag(byte[] deliveryTag) {
+        transfer.setDeliveryTag(new Binary(deliveryTag));
+        return this;
+    }
+
+    public TransferInjectAction withDeliveryTag(Binary deliveryTag) {
+        transfer.setDeliveryTag(deliveryTag);
+        return this;
+    }
+
+    public TransferInjectAction withMessageFormat(long messageFormat) {
+        transfer.setMessageFormat(UnsignedInteger.valueOf(messageFormat));
+        return this;
+    }
+
+    public TransferInjectAction withSettled(Boolean settled) {
+        transfer.setSettled(settled);
+        return this;
+    }
+
+    public TransferInjectAction withSettled(boolean settled) {
+        transfer.setSettled(settled);
+        return this;
+    }
+
+    public TransferInjectAction withMore(boolean more) {
+        transfer.setMore(more);
+        return this;
+    }
+
+    public TransferInjectAction withRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        transfer.setRcvSettleMode(rcvSettleMode.getValue());
+        return this;
+    }
+
+    public TransferInjectAction withState(DeliveryState state) {
+        transfer.setState(state);
+        return this;
+    }
+
+    public DeliveryStateBuilder withState() {
+        return stateBuilder;
+    }
+
+    public TransferInjectAction withResume(boolean resume) {
+        transfer.setResume(resume);
+        return this;
+    }
+
+    public TransferInjectAction withAborted(boolean aborted) {
+        transfer.setAborted(aborted);
+        return this;
+    }
+
+    public TransferInjectAction withBatchable(boolean batchable) {
+        transfer.setBatchable(batchable);
+        return this;
+    }
+
+    public TransferInjectAction withPayload(byte[] payload) {
+        this.payload = Unpooled.wrappedBuffer(payload);
+        return this;
+    }
+
+    public TransferInjectAction withPayload(ByteBuf payload) {
+        this.payload = payload;
+        return this;
+    }
+
+    //----- Allow easy building of an AMQP message in the payload
+
+    public HeaderBuilder withHeader() {
+        return new HeaderBuilder();
+    }
+
+    public DeliveryAnnotationsBuilder withDeliveryAnnotations() {
+        return new DeliveryAnnotationsBuilder();
+    }
+
+    public MessageAnnotationsBuilder withMessageAnnotations() {
+        return new MessageAnnotationsBuilder();
+    }
+
+    public PropertiesBuilder withProperties() {
+        return new PropertiesBuilder();
+    }
+
+    public ApplicationPropertiesBuilder withApplicationProperties() {
+        return new ApplicationPropertiesBuilder();
+    }
+
+    public BodySectionBuilder withBody() {
+        return new BodySectionBuilder();
+    }
+
+    public FooterBuilder withFooter() {
+        return new FooterBuilder();
+    }
+
+    private Header getOrCreateHeader() {
+        if (header == null) {
+            header = new Header();
+        }
+        return header;
+    }
+
+    private DeliveryAnnotations getOrCreateDeliveryAnnotations() {
+        if (deliveryAnnotations == null) {
+            deliveryAnnotations = new DeliveryAnnotations();
+        }
+        return deliveryAnnotations;
+    }
+
+    private MessageAnnotations getOrCreateMessageAnnotations() {
+        if (messageAnnotations == null) {
+            messageAnnotations = new MessageAnnotations();
+        }
+        return messageAnnotations;
+    }
+
+    private Properties getOrCreateProperties() {
+        if (properties == null) {
+            properties = new Properties();
+        }
+        return properties;
+    }
+
+    private ApplicationProperties getOrCreateApplicationProperties() {
+        if (applicationProperties == null) {
+            applicationProperties = new ApplicationProperties();
+        }
+        return applicationProperties;
+    }
+
+    private Footer getOrCreateFooter() {
+        if (footer == null) {
+            footer = new Footer();
+        }
+        return footer;
+    }
+
+    private ByteBuf encodePayload() {
+        org.apache.qpid.protonj2.test.driver.codec.Codec codec =
+            org.apache.qpid.protonj2.test.driver.codec.Codec.Factory.create();
+        ByteBuf buffer = Unpooled.buffer();
+
+        if (header != null) {
+            codec.putDescribedType(header);
+        }
+        if (deliveryAnnotations != null) {
+            codec.putDescribedType(deliveryAnnotations);
+        }
+        if (messageAnnotations != null) {
+            codec.putDescribedType(messageAnnotations);
+        }
+        if (properties != null) {
+            codec.putDescribedType(properties);
+        }
+        if (applicationProperties != null) {
+            codec.putDescribedType(applicationProperties);
+        }
+        if (body != null) {
+            codec.putDescribedType(body);
+        }
+        if (footer != null) {
+            codec.putDescribedType(footer);
+        }
+
+        codec.encode(buffer);
+
+        return buffer;
+    }
+
+    protected abstract class SectionBuilder {
+
+        public TransferInjectAction also() {
+            return TransferInjectAction.this;
+        }
+    }
+
+    public final class HeaderBuilder extends SectionBuilder {
+
+        public HeaderBuilder withDurability(boolean durable) {
+            getOrCreateHeader().setDurable(durable);
+            return this;
+        }
+
+        public HeaderBuilder withPriority(byte priority) {
+            getOrCreateHeader().setPriority(UnsignedByte.valueOf(priority));
+            return this;
+        }
+
+        public HeaderBuilder withTimeToLive(long ttl) {
+            getOrCreateHeader().setTtl(UnsignedInteger.valueOf(ttl));
+            return this;
+        }
+
+        public HeaderBuilder withFirstAcquirer(boolean first) {
+            getOrCreateHeader().setFirstAcquirer(first);
+            return this;
+        }
+
+        public HeaderBuilder withDeliveryCount(long count) {
+            getOrCreateHeader().setDeliveryCount(UnsignedInteger.valueOf(count));
+            return this;
+        }
+    }
+
+    public final class DeliveryAnnotationsBuilder extends SectionBuilder {
+
+        public DeliveryAnnotationsBuilder withAnnotation(String key, Object value) {
+            getOrCreateDeliveryAnnotations().setSymbolKeyedAnnotation(key, value);
+            return this;
+        }
+
+        public DeliveryAnnotationsBuilder withAnnotation(Symbol key, Object value) {
+            getOrCreateDeliveryAnnotations().setSymbolKeyedAnnotation(key, value);
+            return this;
+        }
+    }
+
+    public final class MessageAnnotationsBuilder extends SectionBuilder {
+
+        public MessageAnnotationsBuilder withAnnotation(String key, Object value) {
+            getOrCreateMessageAnnotations().setSymbolKeyedAnnotation(key, value);
+            return this;
+        }
+
+        public MessageAnnotationsBuilder withAnnotation(Symbol key, Object value) {
+            getOrCreateMessageAnnotations().setSymbolKeyedAnnotation(key, value);
+            return this;
+        }
+    }
+
+    public final class PropertiesBuilder extends SectionBuilder {
+
+        public PropertiesBuilder withMessageId(Object value) {
+            getOrCreateProperties().setMessageId(value);
+            return this;
+        }
+
+        public PropertiesBuilder withUserID(Binary value) {
+            getOrCreateProperties().setUserId(value);
+            return this;
+        }
+
+        public PropertiesBuilder withTo(String value) {
+            getOrCreateProperties().setTo(value);
+            return this;
+        }
+
+        public PropertiesBuilder withSubject(String value) {
+            getOrCreateProperties().setSubject(value);
+            return this;
+        }
+
+        public PropertiesBuilder withReplyTp(String value) {
+            getOrCreateProperties().setReplyTo(value);
+            return this;
+        }
+
+        public PropertiesBuilder withCorrelationId(Object value) {
+            getOrCreateProperties().setCorrelationId(value);
+            return this;
+        }
+
+        public PropertiesBuilder withContentType(String value) {
+            getOrCreateProperties().setContentType(Symbol.valueOf(value));
+            return this;
+        }
+
+        public PropertiesBuilder withContentType(Symbol value) {
+            getOrCreateProperties().setContentType(value);
+            return this;
+        }
+
+        public PropertiesBuilder withContentEncoding(String value) {
+            getOrCreateProperties().setContentEncoding(Symbol.valueOf(value));
+            return this;
+        }
+
+        public PropertiesBuilder withContentEncoding(Symbol value) {
+            getOrCreateProperties().setContentEncoding(value);
+            return this;
+        }
+
+        public PropertiesBuilder withAbsoluteExpiryTime(long value) {
+            getOrCreateProperties().setAbsoluteExpiryTime(new Date(value));
+            return this;
+        }
+
+        public PropertiesBuilder withCreationTime(long value) {
+            getOrCreateProperties().setCreationTime(new Date(value));
+            return this;
+        }
+
+        public PropertiesBuilder withGroupId(String value) {
+            getOrCreateProperties().setGroupId(value);
+            return this;
+        }
+
+        public PropertiesBuilder withGroupSequence(long value) {
+            getOrCreateProperties().setGroupSequence(UnsignedInteger.valueOf(value));
+            return this;
+        }
+
+        public PropertiesBuilder withReplyToGroupId(String value) {
+            getOrCreateProperties().setReplyToGroupId(value);
+            return this;
+        }
+    }
+
+    public final class ApplicationPropertiesBuilder extends SectionBuilder {
+
+        public ApplicationPropertiesBuilder withApplicationProperty(String key, Object value) {
+            getOrCreateApplicationProperties().setApplicationProperty(key, value);
+            return this;
+        }
+    }
+
+    public final class BodySectionBuilder extends SectionBuilder {
+
+        public BodySectionBuilder withString(String body) {
+            TransferInjectAction.this.body = new AmqpValue(body);
+            return this;
+        }
+
+        public BodySectionBuilder withData(byte[] body) {
+            TransferInjectAction.this.body = new Data(new Binary(body));
+            return this;
+        }
+
+        public BodySectionBuilder withData(Binary body) {
+            TransferInjectAction.this.body = new Data(body);
+            return this;
+        }
+
+        public BodySectionBuilder withSequence(List<Object> sequence) {
+            TransferInjectAction.this.body = new AmqpSequence(sequence);
+            return this;
+        }
+
+        public BodySectionBuilder withDescribed(DescribedType described) {
+            TransferInjectAction.this.body = new AmqpValue(described);
+            return this;
+        }
+    }
+
+    public final class FooterBuilder extends SectionBuilder {
+
+        public FooterBuilder withFooter(Object key, Object value) {
+            getOrCreateFooter().setFooterProperty(key, value);
+            return this;
+        }
+    }
+
+    public final class DeliveryStateBuilder {
+
+        public TransferInjectAction accepted() {
+            withState(Accepted.getInstance());
+            return TransferInjectAction.this;
+        }
+
+        public TransferInjectAction released() {
+            withState(Released.getInstance());
+            return TransferInjectAction.this;
+        }
+
+        public TransferInjectAction rejected() {
+            withState(new Rejected());
+            return TransferInjectAction.this;
+        }
+
+        public TransferInjectAction rejected(String condition, String description) {
+            withState(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return TransferInjectAction.this;
+        }
+
+        public TransferInjectAction modified() {
+            withState(new Modified());
+            return TransferInjectAction.this;
+        }
+
+        public TransferInjectAction modified(boolean failed) {
+            withState(new Modified());
+            return TransferInjectAction.this;
+        }
+
+        public TransferInjectAction modified(boolean failed, boolean undeliverableHere) {
+            withState(new Modified());
+            return TransferInjectAction.this;
+        }
+
+        public TransactionalStateBuilder transactional() {
+            TransactionalStateBuilder builder = new TransactionalStateBuilder(TransferInjectAction.this);
+            withState(builder.getState());
+            return builder;
+        }
+    }
+
+    //----- Provide a complex builder for Transactional DeliveryState
+
+    public static class TransactionalStateBuilder {
+
+        private final TransferInjectAction action;
+        private final TransactionalState state = new TransactionalState();
+
+        public TransactionalStateBuilder(TransferInjectAction action) {
+            this.action = action;
+        }
+
+        public TransactionalState getState() {
+            return state;
+        }
+
+        public TransferInjectAction also() {
+            return action;
+        }
+
+        public TransferInjectAction and() {
+            return action;
+        }
+
+        public TransactionalStateBuilder withTxnId(byte[] txnId) {
+            state.setTxnId(new Binary(txnId));
+            return this;
+        }
+
+        public TransactionalStateBuilder withTxnId(Binary txnId) {
+            state.setTxnId(txnId);
+            return this;
+        }
+
+        public TransactionalStateBuilder withOutcome(DeliveryState outcome) {
+            state.setOutcome(outcome);
+            return this;
+        }
+
+        // ----- Add a layer to allow configuring the outcome without specific type dependencies
+
+        public TransactionalStateBuilder withAccepted() {
+            withOutcome(Accepted.getInstance());
+            return this;
+        }
+
+        public TransactionalStateBuilder withReleased() {
+            withOutcome(Released.getInstance());
+            return this;
+        }
+
+        public TransactionalStateBuilder withRejected() {
+            withOutcome(new Rejected());
+            return this;
+        }
+
+        public TransactionalStateBuilder withRejected(String condition, String description) {
+            withOutcome(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return this;
+        }
+
+        public TransactionalStateBuilder withModified() {
+            withOutcome(new Modified());
+            return this;
+        }
+
+        public TransactionalStateBuilder withModified(boolean failed) {
+            withOutcome(new Modified().setDeliveryFailed(failed));
+            return this;
+        }
+
+        public TransactionalStateBuilder withModified(boolean failed, boolean undeliverableHere) {
+            withOutcome(new Modified().setDeliveryFailed(failed).setUndeliverableHere(undeliverableHere));
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/NettyReadableBuffer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/NettyReadableBuffer.java
new file mode 100644
index 0000000..4b3d0d7
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/NettyReadableBuffer.java
@@ -0,0 +1,216 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.StandardCharsets;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * ReadableBuffer implementation that wraps a Netty ByteBuf
+ */
+public class NettyReadableBuffer implements ReadableBuffer {
+
+    private ByteBuf buffer;
+
+    public NettyReadableBuffer(ByteBuf buffer) {
+        this.buffer = buffer;
+    }
+
+    public ByteBuf getBuffer() {
+        return buffer;
+    }
+
+    @Override
+    public int capacity() {
+        return buffer.capacity();
+    }
+
+    @Override
+    public boolean hasArray() {
+        return buffer.hasArray();
+    }
+
+    @Override
+    public byte[] array() {
+        return buffer.array();
+    }
+
+    @Override
+    public int arrayOffset() {
+        return buffer.arrayOffset();
+    }
+
+    @Override
+    public ReadableBuffer reclaimRead() {
+        return this;
+    }
+
+    @Override
+    public byte get() {
+        return buffer.readByte();
+    }
+
+    @Override
+    public byte get(int index) {
+        return buffer.getByte(index);
+    }
+
+    @Override
+    public int getInt() {
+        return buffer.readInt();
+    }
+
+    @Override
+    public long getLong() {
+        return buffer.readLong();
+    }
+
+    @Override
+    public short getShort() {
+        return buffer.readShort();
+    }
+
+    @Override
+    public float getFloat() {
+        return buffer.readFloat();
+    }
+
+    @Override
+    public double getDouble() {
+        return buffer.readDouble();
+    }
+
+    @Override
+    public ReadableBuffer get(byte[] target, int offset, int length) {
+        buffer.readBytes(target, offset, length);
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer get(byte[] target) {
+        buffer.readBytes(target);
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer get(WritableBuffer target) {
+        int start = target.position();
+
+        if (buffer.hasArray()) {
+            target.put(buffer.array(), buffer.arrayOffset() + buffer.readerIndex(), buffer.readableBytes());
+        } else {
+            target.put(buffer.nioBuffer());
+        }
+
+        int written = target.position() - start;
+
+        buffer.readerIndex(buffer.readerIndex() + written);
+
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer slice() {
+        return new NettyReadableBuffer(buffer.slice());
+    }
+
+    @Override
+    public ReadableBuffer flip() {
+        buffer.setIndex(0, buffer.readerIndex());
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer limit(int limit) {
+        buffer.writerIndex(limit);
+        return this;
+    }
+
+    @Override
+    public int limit() {
+        return buffer.writerIndex();
+    }
+
+    @Override
+    public ReadableBuffer position(int position) {
+        buffer.readerIndex(position);
+        return this;
+    }
+
+    @Override
+    public int position() {
+        return buffer.readerIndex();
+    }
+
+    @Override
+    public ReadableBuffer mark() {
+        buffer.markReaderIndex();
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer reset() {
+        buffer.resetReaderIndex();
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer rewind() {
+        buffer.readerIndex(0);
+        return this;
+    }
+
+    @Override
+    public ReadableBuffer clear() {
+        buffer.setIndex(0, buffer.capacity());
+        return this;
+    }
+
+    @Override
+    public int remaining() {
+        return buffer.readableBytes();
+    }
+
+    @Override
+    public boolean hasRemaining() {
+        return buffer.isReadable();
+    }
+
+    @Override
+    public ReadableBuffer duplicate() {
+        return new NettyReadableBuffer(buffer.duplicate());
+    }
+
+    @Override
+    public ByteBuffer byteBuffer() {
+        return buffer.nioBuffer();
+    }
+
+    @Override
+    public String readUTF8() throws CharacterCodingException {
+        return buffer.toString(StandardCharsets.UTF_8);
+    }
+
+    @Override
+    public String readString(CharsetDecoder decoder) throws CharacterCodingException {
+        return buffer.toString(StandardCharsets.UTF_8);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/NettyWritableBuffer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/NettyWritableBuffer.java
new file mode 100644
index 0000000..b7f3ec8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/NettyWritableBuffer.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Writable Buffer implementation based on a Netty ByteBuf
+ */
+public class NettyWritableBuffer implements WritableBuffer {
+
+    public static final int INITIAL_CAPACITY = 1024;
+
+    public ByteBuf nettyBuffer;
+
+    public NettyWritableBuffer() {
+        nettyBuffer = Unpooled.buffer(INITIAL_CAPACITY);
+    }
+
+    public NettyWritableBuffer(ByteBuf buffer) {
+        nettyBuffer = buffer;
+    }
+
+    public ByteBuf getBuffer() {
+        return nettyBuffer;
+    }
+
+    @Override
+    public void put(byte b) {
+        nettyBuffer.writeByte(b);
+    }
+
+    @Override
+    public void putFloat(float f) {
+        nettyBuffer.writeFloat(f);
+    }
+
+    @Override
+    public void putDouble(double d) {
+        nettyBuffer.writeDouble(d);
+    }
+
+    @Override
+    public void put(byte[] src, int offset, int length) {
+        nettyBuffer.writeBytes(src, offset, length);
+    }
+
+    @Override
+    public void put(ByteBuffer payload) {
+        nettyBuffer.writeBytes(payload);
+    }
+
+    public void put(ByteBuf payload) {
+        nettyBuffer.writeBytes(payload);
+    }
+
+    @Override
+    public void putShort(short s) {
+        nettyBuffer.writeShort(s);
+    }
+
+    @Override
+    public void putInt(int i) {
+        nettyBuffer.writeInt(i);
+    }
+
+    @Override
+    public void putLong(long l) {
+        nettyBuffer.writeLong(l);
+    }
+
+    @Override
+    public void put(String value) {
+        nettyBuffer.writeCharSequence(value, StandardCharsets.UTF_8);
+    }
+
+    @Override
+    public boolean hasRemaining() {
+        return nettyBuffer.writerIndex() < nettyBuffer.maxCapacity();
+    }
+
+    @Override
+    public int remaining() {
+        return nettyBuffer.maxCapacity() - nettyBuffer.writerIndex();
+    }
+
+    @Override
+    public void ensureRemaining(int remaining) {
+        nettyBuffer.ensureWritable(remaining);
+    }
+
+    @Override
+    public int position() {
+        return nettyBuffer.writerIndex();
+    }
+
+    @Override
+    public void position(int position) {
+        nettyBuffer.writerIndex(position);
+    }
+
+    @Override
+    public int limit() {
+        return nettyBuffer.capacity();
+    }
+
+    @Override
+    public void put(ReadableBuffer buffer) {
+        buffer.get(this);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/ReadableBuffer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/ReadableBuffer.java
new file mode 100644
index 0000000..49da1cf
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/ReadableBuffer.java
@@ -0,0 +1,549 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.InvalidMarkException;
+import java.nio.ReadOnlyBufferException;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Interface to abstract a buffer, similar to {@link WritableBuffer}
+ */
+public interface ReadableBuffer {
+
+    /**
+     * Returns the capacity of the backing buffer of this ReadableBuffer
+     * @return the capacity of the backing buffer of this ReadableBuffer
+     */
+    int capacity();
+
+    /**
+     * Returns true if this ReadableBuffer is backed by an array which can be
+     * accessed by the {@link #array()} and {@link #arrayOffset()} methods.
+     *
+     * @return true if the buffer is backed by a primitive array.
+     */
+    boolean hasArray();
+
+    /**
+     * Returns the primitive array that backs this buffer if one exists and the
+     * buffer is not read-only.  The caller should have checked the {@link #hasArray()}
+     * method before calling this method.
+     *
+     * @return the array that backs this buffer is available.
+     *
+     * @throws UnsupportedOperationException if this {@link ReadableBuffer} doesn't support array access.
+     * @throws ReadOnlyBufferException if the ReadableBuffer is read-only.
+     */
+    byte[] array();
+
+    /**
+     * Returns the offset into the backing array of the first element in the buffer. The caller
+     * should have checked the {@link #hasArray()} method before calling this method.
+     *
+     * @return the offset into the backing array of the first element in the buffer.
+     *
+     * @throws UnsupportedOperationException if this {@link ReadableBuffer} doesn't support array access.
+     * @throws ReadOnlyBufferException if the ReadableBuffer is read-only.
+     */
+    int arrayOffset();
+
+    /**
+     * Compact the backing storage of this ReadableBuffer, possibly freeing previously-read
+     * portions of pooled data or reducing the number of backing arrays if present.
+     * <p>
+     * This is an optional operation and care should be taken in its implementation.
+     *
+     * @return a reference to this buffer
+     */
+    ReadableBuffer reclaimRead();
+
+    /**
+     * Reads the byte at the current position and advances the position by 1.
+     *
+     * @return the byte at the current position.
+     *
+     * @throws BufferUnderflowException if the buffer position has reached the limit.
+     */
+    byte get();
+
+    /**
+     * Reads the byte at the given index, the buffer position is not affected.
+     *
+     * @param index
+     *      The index in the buffer from which to read the byte.
+     *
+     * @return the byte value stored at the target index.
+     *
+     * @throws IndexOutOfBoundsException if the index is not in range for this buffer.
+     */
+    byte get(int index);
+
+    /**
+     * Reads four bytes from the buffer and returns them as an integer value.  The
+     * buffer position is advanced by four byes.
+     *
+     * @return and integer value composed of bytes read from the buffer.
+     *
+     * @throws BufferUnderflowException if the buffer position has reached the limit.
+     */
+    int getInt();
+
+    /**
+     * Reads eight bytes from the buffer and returns them as an long value.  The
+     * buffer position is advanced by eight byes.
+     *
+     * @return and long value composed of bytes read from the buffer.
+     *
+     * @throws BufferUnderflowException if the buffer position has reached the limit.
+     */
+    long getLong();
+
+    /**
+     * Reads two bytes from the buffer and returns them as an short value.  The
+     * buffer position is advanced by two byes.
+     *
+     * @return and short value composed of bytes read from the buffer.
+     *
+     * @throws BufferUnderflowException if the buffer position has reached the limit.
+     */
+    short getShort();
+
+    /**
+     * Reads four bytes from the buffer and returns them as an float value.  The
+     * buffer position is advanced by four byes.
+     *
+     * @return and float value composed of bytes read from the buffer.
+     *
+     * @throws BufferUnderflowException if the buffer position has reached the limit.
+     */
+    float getFloat();
+
+    /**
+     * Reads eight bytes from the buffer and returns them as an double value.  The
+     * buffer position is advanced by eight byes.
+     *
+     * @return and double value composed of bytes read from the buffer.
+     *
+     * @throws BufferUnderflowException if the buffer position has reached the limit.
+     */
+    double getDouble();
+
+    /**
+     * A bulk read method that copies bytes from this buffer into the target byte array.
+     *
+     * @param target
+     *      The byte array to copy bytes read from this buffer.
+     * @param offset
+     *      The offset into the given array where the copy starts.
+     * @param length
+     *      The number of bytes to copy into the target array.
+     *
+     * @return a reference to this ReadableBuffer instance.
+     *
+     * @throws BufferUnderflowException if the are less readable bytes than the array length.
+     * @throws IndexOutOfBoundsException if the offset or length values are invalid.
+     */
+    ReadableBuffer get(final byte[] target, final int offset, final int length);
+
+    /**
+     * A bulk read method that copies bytes from this buffer into the target byte array.
+     *
+     * @param target
+     *      The byte array to copy bytes read from this buffer.
+     *
+     * @return a reference to this ReadableBuffer instance.
+     *
+     * @throws BufferUnderflowException if the are less readable bytes than the array length.
+     */
+    ReadableBuffer get(final byte[] target);
+
+    /**
+     * Copy data from this buffer to the target buffer starting from the current
+     * position and continuing until either this buffer's remaining bytes are
+     * consumed or the target is full.
+     *
+     * @param target
+     *      The WritableBuffer to transfer this buffer's data to.
+     *
+     * @return a reference to this ReadableBuffer instance.
+     */
+    ReadableBuffer get(WritableBuffer target);
+
+    /**
+     * Creates a new ReadableBuffer instance that is a view of the readable portion of
+     * this buffer.  The position will be set to zero and the limit and the reported capacity
+     * will match the value returned by this buffer's {@link #remaining()} method, the mark
+     * will be undefined.
+     *
+     * @return a new ReadableBuffer that is a view of the readable portion of this buffer.
+     */
+    ReadableBuffer slice();
+
+    /**
+     * Sets the buffer limit to the current position and the position is set to zero, the
+     * mark value reset to undefined.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     */
+    ReadableBuffer flip();
+
+    /**
+     * Sets the current read limit of this buffer to the given value.  If the buffer mark
+     * value is defined and is larger than the limit the mark will be discarded.  If the
+     * position is larger than the new limit it will be reset to the new limit.
+     *
+     * @param limit
+     *      The new read limit to set for this buffer.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     *
+     * @throws IllegalArgumentException if the limit value is negative or greater than the capacity.
+     */
+    ReadableBuffer limit(int limit);
+
+    /**
+     * @return the current value of this buffer's limit.
+     */
+    int limit();
+
+    /**
+     * Sets the current position of this buffer to the given value.  If the buffer mark
+     * value is defined and is larger than the newly set position is must be discarded.
+     *
+     * @param position
+     *      The new position to set for this buffer.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     *
+     * @throws IllegalArgumentException if the position value is negative or greater than the limit.
+     */
+    ReadableBuffer position(int position);
+
+    /**
+     * @return the current position from which the next read operation will start.
+     */
+    int position();
+
+    /**
+     * Mark the current position of this buffer which can be returned to after a
+     * read operation by calling {@link #reset()}.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     */
+    ReadableBuffer mark();
+
+    /**
+     * Reset the buffer's position to a previously marked value, the mark should remain
+     * set after calling this method.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     *
+     * @throws InvalidMarkException if the mark value is undefined.
+     */
+    ReadableBuffer reset();
+
+    /**
+     * Resets the buffer position to zero and clears and previously set mark.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     */
+    ReadableBuffer rewind();
+
+    /**
+     * Resets the buffer position to zero and sets the limit to the buffer capacity,
+     * the mark value is discarded if set.
+     *
+     * @return a reference to this {@link ReadableBuffer}.
+     */
+    ReadableBuffer clear();
+
+    /**
+     * @return the remaining number of readable bytes in this buffer.
+     */
+    int remaining();
+
+    /**
+     * @return true if there are readable bytes still remaining in this buffer.
+     */
+    boolean hasRemaining();
+
+    /**
+     * Creates a duplicate {@link ReadableBuffer} to this instance.
+     * <p>
+     * The duplicated buffer will have the same position, limit and mark as this
+     * buffer.  The two buffers share the same backing data.
+     *
+     * @return a duplicate of this {@link ReadableBuffer}.
+     */
+    ReadableBuffer duplicate();
+
+    /**
+     * @return a ByteBuffer view of the current readable portion of this buffer.
+     */
+    ByteBuffer byteBuffer();
+
+    /**
+     * Reads a UTF-8 encoded String from the buffer starting the decode at the
+     * current position and reading until the limit is reached.  The position
+     * is advanced to the limit once this method returns.  If there is no bytes
+     * remaining in the buffer when this method is called a null is returned.
+     *
+     * @return a string decoded from the remaining bytes in this buffer.
+     *
+     * @throws CharacterCodingException if the encoding is invalid for any reason.
+     */
+    String readUTF8() throws CharacterCodingException;
+
+    /**
+     * Decodes a String from the buffer using the provided {@link CharsetDecoder}
+     * starting the decode at the current position and reading until the limit is
+     * reached.  The position is advanced to the limit once this method returns.
+     * If there is no bytes remaining in the buffer when this method is called a
+     * null is returned.
+     *
+     * @param decoder
+     *      The {@link CharsetDecoder} to use when reading the string value.
+     *
+     * @return a string decoded from the remaining bytes in this buffer.
+     *
+     * @throws CharacterCodingException if the encoding is invalid for any reason.
+     */
+    String readString(CharsetDecoder decoder) throws CharacterCodingException;
+
+    final class ByteBufferReader implements ReadableBuffer {
+
+        private ByteBuffer buffer;
+
+        public static ByteBufferReader allocate(int size) {
+            ByteBuffer allocated = ByteBuffer.allocate(size);
+            return new ByteBufferReader(allocated);
+        }
+
+        public static ByteBufferReader wrap(ByteBuffer buffer) {
+            return new ByteBufferReader(buffer);
+        }
+
+        public static ByteBufferReader wrap(byte[] array) {
+            return new ByteBufferReader(ByteBuffer.wrap(array));
+        }
+
+        public ByteBufferReader(ByteBuffer buffer) {
+            this.buffer = buffer;
+        }
+
+        @Override
+        public int capacity() {
+            return buffer.capacity();
+        }
+
+        @Override
+        public byte get() {
+            return buffer.get();
+        }
+
+        @Override
+        public byte get(int index) {
+            return buffer.get(index);
+        }
+
+        @Override
+        public int getInt() {
+            return buffer.getInt();
+        }
+
+        @Override
+        public long getLong() {
+            return buffer.getLong();
+        }
+
+        @Override
+        public short getShort() {
+            return buffer.getShort();
+        }
+
+        @Override
+        public float getFloat() {
+            return buffer.getFloat();
+        }
+
+        @Override
+        public double getDouble() {
+            return buffer.getDouble();
+        }
+
+        @Override
+        public int limit() {
+            return buffer.limit();
+        }
+
+        @Override
+        public ReadableBuffer get(byte[] data, int offset, int length) {
+            buffer.get(data, offset, length);
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer get(byte[] data) {
+            buffer.get(data);
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer flip() {
+            buffer.flip();
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer position(int position) {
+            buffer.position(position);
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer slice() {
+            return new ByteBufferReader(buffer.slice());
+        }
+
+        @Override
+        public ReadableBuffer limit(int limit) {
+            buffer.limit(limit);
+            return this;
+        }
+
+        @Override
+        public int remaining() {
+            return buffer.remaining();
+        }
+
+        @Override
+        public int position() {
+            return buffer.position();
+        }
+
+        @Override
+        public boolean hasRemaining() {
+            return buffer.hasRemaining();
+        }
+
+        @Override
+        public ReadableBuffer duplicate() {
+            return new ByteBufferReader(buffer.duplicate());
+        }
+
+        @Override
+        public ByteBuffer byteBuffer() {
+            return buffer;
+        }
+
+        @Override
+        public String readUTF8() {
+            return StandardCharsets.UTF_8.decode(buffer).toString();
+        }
+
+        @Override
+        public String readString(CharsetDecoder decoder) throws CharacterCodingException {
+            return decoder.decode(buffer).toString();
+        }
+
+        @Override
+        public boolean hasArray() {
+            return buffer.hasArray();
+        }
+
+        @Override
+        public byte[] array() {
+            return buffer.array();
+        }
+
+        @Override
+        public int arrayOffset() {
+            return buffer.arrayOffset();
+        }
+
+        @Override
+        public ReadableBuffer reclaimRead() {
+            // Don't compact ByteBuffer due to the expense of the copy
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer mark() {
+            buffer.mark();
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer reset() {
+            buffer.reset();
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer rewind() {
+            buffer.rewind();
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer clear() {
+            buffer.clear();
+            return this;
+        }
+
+        @Override
+        public ReadableBuffer get(WritableBuffer target) {
+            target.put(buffer);
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return buffer.toString();
+        }
+
+        @Override
+        public int hashCode() {
+            return buffer.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+
+            if (!(other instanceof ReadableBuffer)) {
+                return false;
+            }
+
+            ReadableBuffer readable = (ReadableBuffer) other;
+            if (this.remaining() != readable.remaining()) {
+                return false;
+            }
+
+            return buffer.equals(readable.byteBuffer());
+        }
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/WritableBuffer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/WritableBuffer.java
new file mode 100644
index 0000000..c0558e7
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/buffer/WritableBuffer.java
@@ -0,0 +1,263 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+
+public interface WritableBuffer {
+
+    void put(byte b);
+
+    void putFloat(float f);
+
+    void putDouble(double d);
+
+    void put(byte[] src, int offset, int length);
+
+    void putShort(short s);
+
+    void putInt(int i);
+
+    void putLong(long l);
+
+    boolean hasRemaining();
+
+    default void ensureRemaining(int requiredRemaining) {
+        // No-op to allow for drop in updates
+    }
+
+    int remaining();
+
+    int position();
+
+    void position(int position);
+
+    void put(ByteBuffer payload);
+
+    void put(ReadableBuffer payload);
+
+    default void put(final String value) {
+        final int length = value.length();
+
+        for (int i = 0; i < length; i++) {
+            int c = value.charAt(i);
+            if ((c & 0xFF80) == 0) {
+                // U+0000..U+007F
+                put((byte) c);
+            } else if ((c & 0xF800) == 0)  {
+                // U+0080..U+07FF
+                put((byte) (0xC0 | ((c >> 6) & 0x1F)));
+                put((byte) (0x80 | (c & 0x3F)));
+            } else if ((c & 0xD800) != 0xD800 || (c > 0xDBFF)) {
+                // U+0800..U+FFFF - excluding surrogate pairs
+                put((byte) (0xE0 | ((c >> 12) & 0x0F)));
+                put((byte) (0x80 | ((c >> 6) & 0x3F)));
+                put((byte) (0x80 | (c & 0x3F)));
+            } else {
+                int low;
+
+                if ((++i == length) || ((low = value.charAt(i)) & 0xDC00) != 0xDC00) {
+                    throw new IllegalArgumentException("String contains invalid Unicode code points");
+                }
+
+                c = 0x010000 + ((c & 0x03FF) << 10) + (low & 0x03FF);
+
+                put((byte) (0xF0 | ((c >> 18) & 0x07)));
+                put((byte) (0x80 | ((c >> 12) & 0x3F)));
+                put((byte) (0x80 | ((c >> 6) & 0x3F)));
+                put((byte) (0x80 | (c & 0x3F)));
+            }
+        }
+    }
+
+    int limit();
+
+    class ByteBufferWrapper implements WritableBuffer {
+        private final ByteBuffer _buf;
+
+        public ByteBufferWrapper(ByteBuffer buf) {
+            _buf = buf;
+        }
+
+        @Override
+        public void put(byte b) {
+            _buf.put(b);
+        }
+
+        @Override
+        public void putFloat(float f) {
+            _buf.putFloat(f);
+        }
+
+        @Override
+        public void putDouble(double d) {
+            _buf.putDouble(d);
+        }
+
+        @Override
+        public void put(byte[] src, int offset, int length) {
+            _buf.put(src, offset, length);
+        }
+
+        @Override
+        public void putShort(short s) {
+            _buf.putShort(s);
+        }
+
+        @Override
+        public void putInt(int i) {
+            _buf.putInt(i);
+        }
+
+        @Override
+        public void putLong(long l) {
+            _buf.putLong(l);
+        }
+
+        @Override
+        public boolean hasRemaining() {
+            return _buf.hasRemaining();
+        }
+
+        @Override
+        public void ensureRemaining(int remaining) {
+            if (remaining < 0) {
+                throw new IllegalArgumentException("Required remaining bytes cannot be negative");
+            }
+
+            if (_buf.remaining() < remaining) {
+                IndexOutOfBoundsException cause = new IndexOutOfBoundsException(String.format(
+                    "Requested min remaining bytes(%d) exceeds remaining(%d) in underlying ByteBuffer: %s",
+                    remaining, _buf.remaining(), _buf));
+
+                throw (BufferOverflowException) new BufferOverflowException().initCause(cause);
+            }
+        }
+
+        @Override
+        public int remaining() {
+            return _buf.remaining();
+        }
+
+        @Override
+        public int position() {
+            return _buf.position();
+        }
+
+        @Override
+        public void position(int position) {
+            _buf.position(position);
+        }
+
+        @Override
+        public void put(ByteBuffer src) {
+            _buf.put(src);
+        }
+
+        @Override
+        public void put(ReadableBuffer src) {
+            src.get(this);
+        }
+
+        @Override
+        public void put(final String value) {
+            final int length = value.length();
+
+            int pos = _buf.position();
+
+            for (int i = 0; i < length; i++) {
+                int c = value.charAt(i);
+                try {
+                    if ((c & 0xFF80) == 0) {
+                        // U+0000..U+007F
+                        put(pos++, (byte) c);
+                    } else if ((c & 0xF800) == 0)  {
+                        // U+0080..U+07FF
+                        put(pos++, (byte) (0xC0 | ((c >> 6) & 0x1F)));
+                        put(pos++, (byte) (0x80 | (c & 0x3F)));
+                    } else if ((c & 0xD800) != 0xD800 || (c > 0xDBFF))  {
+                        // U+0800..U+FFFF - excluding surrogate pairs
+                        put(pos++, (byte) (0xE0 | ((c >> 12) & 0x0F)));
+                        put(pos++, (byte) (0x80 | ((c >> 6) & 0x3F)));
+                        put(pos++, (byte) (0x80 | (c & 0x3F)));
+                    } else {
+                        int low;
+
+                        if ((++i == length) || ((low = value.charAt(i)) & 0xDC00) != 0xDC00) {
+                            throw new IllegalArgumentException("String contains invalid Unicode code points");
+                        }
+
+                        c = 0x010000 + ((c & 0x03FF) << 10) + (low & 0x03FF);
+
+                        put(pos++, (byte) (0xF0 | ((c >> 18) & 0x07)));
+                        put(pos++, (byte) (0x80 | ((c >> 12) & 0x3F)));
+                        put(pos++, (byte) (0x80 | ((c >> 6) & 0x3F)));
+                        put(pos++, (byte) (0x80 | (c & 0x3F)));
+                    }
+                }
+                catch(IndexOutOfBoundsException ioobe) {
+                    throw new BufferOverflowException();
+                }
+            }
+
+            // Now move the buffer position to reflect the work done here
+            _buf.position(pos);
+        }
+
+        @Override
+        public int limit() {
+            return _buf.limit();
+        }
+
+        public ByteBuffer byteBuffer() {
+            return _buf;
+        }
+
+        public ReadableBuffer toReadableBuffer() {
+            return ReadableBuffer.ByteBufferReader.wrap((ByteBuffer) _buf.duplicate().flip());
+        }
+
+        @Override
+        public String toString() {
+            return String.format("[pos: %d, limit: %d, remaining:%d]", _buf.position(), _buf.limit(), _buf.remaining());
+        }
+
+        public static ByteBufferWrapper allocate(int size) {
+            ByteBuffer allocated = ByteBuffer.allocate(size);
+            return new ByteBufferWrapper(allocated);
+        }
+
+        public static ByteBufferWrapper wrap(ByteBuffer buffer) {
+            return new ByteBufferWrapper(buffer);
+        }
+
+        public static ByteBufferWrapper wrap(byte[] bytes) {
+            return new ByteBufferWrapper(ByteBuffer.wrap(bytes));
+        }
+
+        private void put(int index, byte value) {
+            if (_buf.hasArray()) {
+                _buf.array()[_buf.arrayOffset() + index] = value;
+            } else {
+                _buf.put(index, value);
+            }
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/AbstractElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/AbstractElement.java
new file mode 100644
index 0000000..374ae1e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/AbstractElement.java
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+abstract class AbstractElement<T> implements Element<T> {
+
+    private Element<?> parent;
+    private Element<?> next;
+    private Element<?> prev;
+
+    AbstractElement(Element<?> parent, Element<?> prev) {
+        this.parent = parent;
+        this.prev = prev;
+    }
+
+    protected boolean isElementOfArray() {
+        return parent instanceof ArrayElement && !(((ArrayElement) parent()).isDescribed() && this == parent.child());
+    }
+
+    @Override
+    public Element<?> next() {
+        return next;
+    }
+
+    @Override
+    public Element<?> prev() {
+        return prev;
+    }
+
+    @Override
+    public Element<?> parent() {
+        return parent;
+    }
+
+    @Override
+    public void setNext(Element<?> elt) {
+        next = elt;
+    }
+
+    @Override
+    public void setPrev(Element<?> elt) {
+        prev = elt;
+    }
+
+    @Override
+    public void setParent(Element<?> elt) {
+        parent = elt;
+    }
+
+    @Override
+    public Element<?> replaceWith(Element<?> elt) {
+        if (parent != null) {
+            elt = parent.checkChild(elt);
+        }
+
+        elt.setPrev(prev);
+        elt.setNext(next);
+        elt.setParent(parent);
+
+        if (prev != null) {
+            prev.setNext(elt);
+        }
+        if (next != null) {
+            next.setPrev(elt);
+        }
+
+        if (parent != null && parent.child() == this) {
+            parent.setChild(elt);
+        }
+
+        return elt;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s[%h]{parent=%h, prev=%h, next=%h}", this.getClass().getSimpleName(), System.identityHashCode(this),
+            System.identityHashCode(parent), System.identityHashCode(prev), System.identityHashCode(next));
+    }
+
+    abstract String startSymbol();
+
+    abstract String stopSymbol();
+
+    @Override
+    public void render(StringBuilder sb) {
+        if (canEnter()) {
+            sb.append(startSymbol());
+            Element<?> el = child();
+            boolean first = true;
+            while (el != null) {
+                if (first) {
+                    first = false;
+                } else {
+                    sb.append(", ");
+                }
+                el.render(sb);
+                el = el.next();
+            }
+            sb.append(stopSymbol());
+        } else {
+            sb.append(getDataType()).append(" ").append(getValue());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ArrayElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ArrayElement.java
new file mode 100644
index 0000000..a2a3e88
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ArrayElement.java
@@ -0,0 +1,456 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+
+import io.netty.buffer.ByteBuf;
+
+class ArrayElement extends AbstractElement<Object[]> {
+
+    private final boolean described;
+    private final Codec.DataType arrayType;
+    private ConstructorType constructorType;
+    private Element<?> first;
+
+    enum ConstructorType {
+        TINY, SMALL, LARGE
+    }
+
+    static ConstructorType TINY = ConstructorType.TINY;
+    static ConstructorType SMALL = ConstructorType.SMALL;
+    static ConstructorType LARGE = ConstructorType.LARGE;
+
+    ArrayElement(Element<?> parent, Element<?> prev, boolean described, Codec.DataType type) {
+        super(parent, prev);
+        this.described = described;
+        this.arrayType = type;
+        if (arrayType == null) {
+            throw new NullPointerException("Array type cannot be null");
+        } else if (arrayType == Codec.DataType.DESCRIBED) {
+            throw new IllegalArgumentException("Array type cannot be DESCRIBED");
+        }
+        switch (arrayType) {
+            case UINT:
+            case ULONG:
+            case LIST:
+                setConstructorType(TINY);
+                break;
+            default:
+                setConstructorType(SMALL);
+        }
+    }
+
+    ConstructorType constructorType() {
+        return constructorType;
+    }
+
+    void setConstructorType(ConstructorType type) {
+        constructorType = type;
+    }
+
+    @Override
+    public int size() {
+        ConstructorType oldConstructorType;
+        int bodySize;
+        int count = 0;
+        do {
+            bodySize = 1; // data type constructor
+            oldConstructorType = constructorType;
+            Element<?> element = first;
+            while (element != null) {
+                count++;
+                bodySize += element.size();
+                element = element.next();
+            }
+        } while (oldConstructorType != constructorType());
+
+        if (isDescribed()) {
+            bodySize++; // 00 instruction
+            if (count != 0) {
+                count--;
+            }
+        }
+
+        if (isElementOfArray()) {
+            ArrayElement parent = (ArrayElement) parent();
+            if (parent.constructorType() == SMALL) {
+                if (count <= 255 && bodySize <= 254) {
+                    bodySize += 2;
+                } else {
+                    parent.setConstructorType(LARGE);
+                    bodySize += 8;
+                }
+            } else {
+                bodySize += 8;
+            }
+        } else {
+
+            if (count <= 255 && bodySize <= 254) {
+                bodySize += 3;
+            } else {
+                bodySize += 9;
+            }
+
+        }
+
+        return bodySize;
+    }
+
+    @Override
+    public Object[] getValue() {
+        if (isDescribed()) {
+            DescribedType[] rVal = new DescribedType[(int) count()];
+            Object descriptor = first == null ? null : first.getValue();
+            Element<?> element = first == null ? null : first.next();
+            int i = 0;
+            while (element != null) {
+                rVal[i++] = new DescribedTypeImpl(descriptor, element.getValue());
+                element = element.next();
+            }
+            return rVal;
+        } else if (arrayType == Codec.DataType.SYMBOL) {
+            Symbol[] rVal = new Symbol[(int) count()];
+            SymbolElement element = (SymbolElement) first;
+            int i = 0;
+            while (element != null) {
+                rVal[i++] = element.getValue();
+                element = (SymbolElement) element.next();
+            }
+            return rVal;
+        } else {
+            Object[] rVal = new Object[(int) count()];
+            Element<?> element = first;
+            int i = 0;
+            while (element != null) {
+                rVal[i++] = element.getValue();
+                element = element.next();
+            }
+            return rVal;
+        }
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.ARRAY;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+
+        final int count = (int) count();
+
+        if (buffer.maxWritableBytes() >= size) {
+            if (!isElementOfArray()) {
+                if (size > 257 || count > 255) {
+                    buffer.writeByte((byte) 0xf0);
+                    buffer.writeInt(size - 5);
+                    buffer.writeInt(count);
+                } else {
+                    buffer.writeByte((byte) 0xe0);
+                    buffer.writeByte((byte) (size - 2));
+                    buffer.writeByte((byte) count);
+                }
+            } else {
+                ArrayElement parent = (ArrayElement) parent();
+                if (parent.constructorType() == SMALL) {
+                    buffer.writeByte((byte) (size - 1));
+                    buffer.writeByte((byte) count);
+                } else {
+                    buffer.writeInt(size - 4);
+                    buffer.writeInt(count);
+                }
+            }
+            Element<?> element = first;
+            if (isDescribed()) {
+                buffer.writeByte((byte) 0);
+                if (element == null) {
+                    buffer.writeByte((byte) 0x40);
+                } else {
+                    element.encode(buffer);
+                    element = element.next();
+                }
+            }
+            switch (arrayType) {
+                case NULL:
+                    buffer.writeByte((byte) 0x40);
+                    break;
+                case BOOL:
+                    buffer.writeByte((byte) 0x56);
+                    break;
+                case UBYTE:
+                    buffer.writeByte((byte) 0x50);
+                    break;
+                case BYTE:
+                    buffer.writeByte((byte) 0x51);
+                    break;
+                case USHORT:
+                    buffer.writeByte((byte) 0x60);
+                    break;
+                case SHORT:
+                    buffer.writeByte((byte) 0x61);
+                    break;
+                case UINT:
+                    switch (constructorType()) {
+                        case TINY:
+                            buffer.writeByte((byte) 0x43);
+                            break;
+                        case SMALL:
+                            buffer.writeByte((byte) 0x52);
+                            break;
+                        case LARGE:
+                            buffer.writeByte((byte) 0x70);
+                            break;
+                    }
+                    break;
+                case INT:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0x54 : (byte) 0x71);
+                    break;
+                case CHAR:
+                    buffer.writeByte((byte) 0x73);
+                    break;
+                case ULONG:
+                    switch (constructorType()) {
+                        case TINY:
+                            buffer.writeByte((byte) 0x44);
+                            break;
+                        case SMALL:
+                            buffer.writeByte((byte) 0x53);
+                            break;
+                        case LARGE:
+                            buffer.writeByte((byte) 0x80);
+                            break;
+                    }
+                    break;
+                case LONG:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0x55 : (byte) 0x81);
+                    break;
+                case TIMESTAMP:
+                    buffer.writeByte((byte) 0x83);
+                    break;
+                case FLOAT:
+                    buffer.writeByte((byte) 0x72);
+                    break;
+                case DOUBLE:
+                    buffer.writeByte((byte) 0x82);
+                    break;
+                case DECIMAL32:
+                    buffer.writeByte((byte) 0x74);
+                    break;
+                case DECIMAL64:
+                    buffer.writeByte((byte) 0x84);
+                    break;
+                case DECIMAL128:
+                    buffer.writeByte((byte) 0x94);
+                    break;
+                case UUID:
+                    buffer.writeByte((byte) 0x98);
+                    break;
+                case BINARY:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0xa0 : (byte) 0xb0);
+                    break;
+                case STRING:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0xa1 : (byte) 0xb1);
+                    break;
+                case SYMBOL:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0xa3 : (byte) 0xb3);
+                    break;
+                case ARRAY:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0xe0 : (byte) 0xf0);
+                    break;
+                case LIST:
+                    buffer.writeByte(constructorType == TINY ? (byte) 0x45 : constructorType == SMALL ? (byte) 0xc0 : (byte) 0xd0);
+                    break;
+                case MAP:
+                    buffer.writeByte(constructorType == SMALL ? (byte) 0xc1 : (byte) 0xd1);
+                    break;
+                case DESCRIBED:
+                    break;
+                default:
+                    break;
+            }
+            while (element != null) {
+                element.encode(buffer);
+                element = element.next();
+            }
+            return size;
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public boolean canEnter() {
+        return true;
+    }
+
+    @Override
+    public Element<?> child() {
+        return first;
+    }
+
+    @Override
+    public void setChild(Element<?> elt) {
+        first = elt;
+    }
+
+    @Override
+    public Element<?> addChild(Element<?> element) {
+        if (isDescribed() || element.getDataType() == arrayType) {
+            first = element;
+            return element;
+        } else {
+            Element<?> replacement = coerce(element);
+            if (replacement != null) {
+                first = replacement;
+                return replacement;
+            }
+            throw new IllegalArgumentException("Attempting to add instance of " + element.getDataType() + " to array of " + arrayType);
+        }
+    }
+
+    private Element<?> coerce(Element<?> element) {
+        switch (arrayType) {
+            case INT:
+                int i;
+                switch (element.getDataType()) {
+                    case BYTE:
+                        i = ((ByteElement) element).getValue().intValue();
+                        break;
+                    case SHORT:
+                        i = ((ShortElement) element).getValue().intValue();
+                        break;
+                    case LONG:
+                        i = ((LongElement) element).getValue().intValue();
+                        break;
+                    default:
+                        return null;
+                }
+                return new IntegerElement(element.parent(), element.prev(), i);
+            case LONG:
+                long l;
+                switch (element.getDataType()) {
+                    case BYTE:
+                        l = ((ByteElement) element).getValue().longValue();
+                        break;
+                    case SHORT:
+                        l = ((ShortElement) element).getValue().longValue();
+                        break;
+                    case INT:
+                        l = ((IntegerElement) element).getValue().longValue();
+                        break;
+                    default:
+                        return null;
+                }
+                return new LongElement(element.parent(), element.prev(), l);
+            case ARRAY:
+                break;
+            case BINARY:
+                break;
+            case BOOL:
+                break;
+            case BYTE:
+                break;
+            case CHAR:
+                break;
+            case DECIMAL128:
+                break;
+            case DECIMAL32:
+                break;
+            case DECIMAL64:
+                break;
+            case DESCRIBED:
+                break;
+            case DOUBLE:
+                break;
+            case FLOAT:
+                break;
+            case LIST:
+                break;
+            case MAP:
+                break;
+            case NULL:
+                break;
+            case SHORT:
+                break;
+            case STRING:
+                break;
+            case SYMBOL:
+                break;
+            case TIMESTAMP:
+                break;
+            case UBYTE:
+                break;
+            case UINT:
+                break;
+            case ULONG:
+                break;
+            case USHORT:
+                break;
+            case UUID:
+                break;
+            default:
+                break;
+        }
+        return null;
+    }
+
+    @Override
+    public Element<?> checkChild(Element<?> element) {
+        if (element.getDataType() != arrayType) {
+            Element<?> replacement = coerce(element);
+            if (replacement != null) {
+                return replacement;
+            }
+            throw new IllegalArgumentException("Attempting to add instance of " + element.getDataType() + " to array of " + arrayType);
+        }
+        return element;
+    }
+
+    public long count() {
+        int count = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            elt = elt.next();
+        }
+        if (isDescribed() && count != 0) {
+            count--;
+        }
+        return count;
+    }
+
+    public boolean isDescribed() {
+        return described;
+    }
+
+    public Codec.DataType getArrayDataType() {
+        return arrayType;
+    }
+
+    @Override
+    String startSymbol() {
+        return String.format("%s%s[", isDescribed() ? "D" : "", getArrayDataType());
+    }
+
+    @Override
+    String stopSymbol() {
+        return "]";
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/AtomicElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/AtomicElement.java
new file mode 100644
index 0000000..1779681
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/AtomicElement.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+abstract class AtomicElement<T> extends AbstractElement<T> {
+
+    AtomicElement(Element<?> parent, Element<?> prev) {
+        super(parent, prev);
+    }
+
+    @Override
+    public Element<?> child() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setChild(Element<?> elt) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canEnter() {
+        return false;
+    }
+
+    @Override
+    public Element<?> checkChild(Element<?> element) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Element<?> addChild(Element<?> element) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    String startSymbol() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    String stopSymbol() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/BinaryElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/BinaryElement.java
new file mode 100644
index 0000000..07559af
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/BinaryElement.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+
+import io.netty.buffer.ByteBuf;
+
+class BinaryElement extends AtomicElement<Binary> {
+
+    private final Binary value;
+
+    BinaryElement(Element<?> parent, Element<?> prev, Binary b) {
+        super(parent, prev);
+        byte[] data = new byte[b.getLength()];
+        System.arraycopy(b.getArray(), 0, data, 0, b.getLength());
+        value = new Binary(data);
+    }
+
+    @Override
+    public int size() {
+        final int length = value.getLength();
+
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (length > 255) {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                    return 4 + length;
+                } else {
+                    return 1 + length;
+                }
+            } else {
+                return 4 + length;
+            }
+        } else {
+            if (length > 255) {
+                return 5 + length;
+            } else {
+                return 2 + length;
+            }
+        }
+    }
+
+    @Override
+    public Binary getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.BINARY;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() < size) {
+            return 0;
+        }
+
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                buffer.writeByte((byte) value.getLength());
+            } else {
+                buffer.writeInt(value.getLength());
+            }
+        } else if (value.getLength() <= 255) {
+            buffer.writeByte((byte) 0xa0);
+            buffer.writeByte((byte) value.getLength());
+        } else {
+            buffer.writeByte((byte) 0xb0);
+            buffer.writeInt(value.getLength());
+        }
+
+        buffer.writeBytes(value.getArray(), 0, value.getLength());
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/BooleanElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/BooleanElement.java
new file mode 100644
index 0000000..2aaed6d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/BooleanElement.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class BooleanElement extends AtomicElement<Boolean> {
+
+    private final boolean value;
+
+    public BooleanElement(Element<?> parent, Element<?> current, boolean b) {
+        super(parent, current);
+        value = b;
+    }
+
+    @Override
+    public int size() {
+        // in non-array parent then there is a single byte encoding, in an array
+        // there is a 1-byte encoding but no
+        // constructor
+        return 1;
+    }
+
+    @Override
+    public Boolean getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.BOOL;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        if (buffer.isWritable()) {
+            if (isElementOfArray()) {
+                buffer.writeByte(value ? (byte) 1 : (byte) 0);
+            } else {
+                buffer.writeByte(value ? (byte) 0x41 : (byte) 0x42);
+            }
+            return 1;
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ByteElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ByteElement.java
new file mode 100644
index 0000000..59d6c43
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ByteElement.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class ByteElement extends AtomicElement<Byte> {
+
+    private final byte value;
+
+    ByteElement(Element<?> parent, Element<?> prev, byte b) {
+        super(parent, prev);
+        value = b;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 1 : 2;
+    }
+
+    @Override
+    public Byte getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.BYTE;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        if (isElementOfArray()) {
+            if (buffer.isWritable()) {
+                buffer.writeByte(value);
+                return 1;
+            }
+        } else {
+            if (buffer.maxWritableBytes() >= 2) {
+                buffer.writeByte((byte) 0x51);
+                buffer.writeByte(value);
+                return 2;
+            }
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/CharElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/CharElement.java
new file mode 100644
index 0000000..036b96a
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/CharElement.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class CharElement extends AtomicElement<Integer> {
+
+    private final int value;
+
+    CharElement(Element<?> parent, Element<?> prev, int i) {
+        super(parent, prev);
+        value = i;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 4 : 5;
+    }
+
+    @Override
+    public Integer getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.CHAR;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        final int size = size();
+        if (size <= buffer.maxWritableBytes()) {
+            if (size == 5) {
+                buffer.writeByte((byte) 0x73);
+            }
+            buffer.writeInt(value);
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Codec.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Codec.java
new file mode 100644
index 0000000..b44d2e4
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Codec.java
@@ -0,0 +1,224 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal128;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal32;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal64;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+public interface Codec {
+
+    public static final class Factory {
+
+        public static Codec create() {
+            return new CodecImpl();
+        }
+    }
+
+    enum DataType {
+        NULL,
+        BOOL,
+        UBYTE,
+        BYTE,
+        USHORT,
+        SHORT,
+        UINT,
+        INT,
+        CHAR,
+        ULONG,
+        LONG,
+        TIMESTAMP,
+        FLOAT,
+        DOUBLE,
+        DECIMAL32,
+        DECIMAL64,
+        DECIMAL128,
+        UUID,
+        BINARY,
+        STRING,
+        SYMBOL,
+        DESCRIBED,
+        ARRAY,
+        LIST,
+        MAP
+    }
+
+    void free();
+
+    void clear();
+
+    long size();
+
+    void rewind();
+
+    DataType next();
+
+    DataType prev();
+
+    boolean enter();
+
+    boolean exit();
+
+    DataType type();
+
+    long encodedSize();
+
+    long encode(ByteBuf buffer);
+
+    long decode(ByteBuf buffer);
+
+    void putList();
+
+    void putMap();
+
+    void putArray(boolean described, DataType type);
+
+    void putDescribed();
+
+    void putNull();
+
+    void putBoolean(boolean b);
+
+    void putUnsignedByte(UnsignedByte ub);
+
+    void putByte(byte b);
+
+    void putUnsignedShort(UnsignedShort us);
+
+    void putShort(short s);
+
+    void putUnsignedInteger(UnsignedInteger ui);
+
+    void putInt(int i);
+
+    void putChar(int c);
+
+    void putUnsignedLong(UnsignedLong ul);
+
+    void putLong(long l);
+
+    void putTimestamp(Date t);
+
+    void putFloat(float f);
+
+    void putDouble(double d);
+
+    void putDecimal32(Decimal32 d);
+
+    void putDecimal64(Decimal64 d);
+
+    void putDecimal128(Decimal128 d);
+
+    void putUUID(UUID u);
+
+    void putBinary(Binary bytes);
+
+    void putBinary(byte[] bytes);
+
+    void putString(String string);
+
+    void putSymbol(Symbol symbol);
+
+    void putObject(Object o);
+
+    void putJavaMap(Map<Object, Object> map);
+
+    void putJavaList(List<Object> list);
+
+    void putDescribedType(DescribedType dt);
+
+    long getList();
+
+    long getMap();
+
+    long getArray();
+
+    boolean isArrayDescribed();
+
+    DataType getArrayType();
+
+    boolean isDescribed();
+
+    boolean isNull();
+
+    boolean getBoolean();
+
+    UnsignedByte getUnsignedByte();
+
+    byte getByte();
+
+    UnsignedShort getUnsignedShort();
+
+    short getShort();
+
+    UnsignedInteger getUnsignedInteger();
+
+    int getInt();
+
+    int getChar();
+
+    UnsignedLong getUnsignedLong();
+
+    long getLong();
+
+    Date getTimestamp();
+
+    float getFloat();
+
+    double getDouble();
+
+    Decimal32 getDecimal32();
+
+    Decimal64 getDecimal64();
+
+    Decimal128 getDecimal128();
+
+    UUID getUUID();
+
+    Binary getBinary();
+
+    String getString();
+
+    Symbol getSymbol();
+
+    Object getObject();
+
+    Map<Object, Object> getJavaMap();
+
+    List<Object> getJavaList();
+
+    Object[] getJavaArray();
+
+    DescribedType getDescribedType();
+
+    String format();
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/CodecImpl.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/CodecImpl.java
new file mode 100644
index 0000000..f1b358f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/CodecImpl.java
@@ -0,0 +1,677 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal128;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal32;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal64;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+public class CodecImpl implements Codec {
+
+    private Element<?> first;
+    private Element<?> current;
+    private Element<?> parent;
+
+    public CodecImpl() {
+    }
+
+    @Override
+    public void free() {
+        first = null;
+        current = null;
+    }
+
+    @Override
+    public void clear() {
+        first = null;
+        current = null;
+        parent = null;
+    }
+
+    @Override
+    public long size() {
+        return first == null ? 0 : first.size();
+    }
+
+    @Override
+    public void rewind() {
+        current = null;
+        parent = null;
+    }
+
+    @Override
+    public DataType next() {
+        Element<?> next = current == null ? (parent == null ? first : parent.child()) : current.next();
+
+        if (next != null) {
+            current = next;
+        }
+        return next == null ? null : next.getDataType();
+    }
+
+    @Override
+    public DataType prev() {
+        Element<?> prev = current == null ? null : current.prev();
+
+        current = prev;
+        return prev == null ? null : prev.getDataType();
+    }
+
+    @Override
+    public boolean enter() {
+        if (current != null && current.canEnter()) {
+            parent = current;
+            current = null;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean exit() {
+        if (parent != null) {
+            Element<?> oldParent = this.parent;
+            current = oldParent;
+            parent = current.parent();
+            return true;
+
+        }
+        return false;
+    }
+
+    @Override
+    public DataType type() {
+        return current == null ? null : current.getDataType();
+    }
+
+    @Override
+    public long encodedSize() {
+        int size = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            size += elt.size();
+            elt = elt.next();
+        }
+        return size;
+    }
+
+    @Override
+    public long encode(ByteBuf buffer) {
+        Element<?> elt = first;
+        int size = 0;
+        while (elt != null) {
+            final int eltSize = elt.size();
+            if (eltSize <= buffer.maxWritableBytes()) {
+                size += elt.encode(buffer);
+            } else {
+                size += eltSize;
+            }
+            elt = elt.next();
+        }
+        return size;
+    }
+
+    @Override
+    public long decode(ByteBuf buffer) {
+        return TypeDecoder.decode(buffer, this);
+    }
+
+    private void putElement(Element<?> element) {
+        if (first == null) {
+            first = element;
+        } else {
+            if (current == null) {
+                if (parent == null) {
+                    first = first.replaceWith(element);
+                    element = first;
+                } else {
+                    element = parent.addChild(element);
+                }
+            } else {
+                if (parent != null) {
+                    element = parent.checkChild(element);
+                }
+                current.setNext(element);
+            }
+        }
+
+        current = element;
+    }
+
+    @Override
+    public void putList() {
+        putElement(new ListElement(parent, current));
+    }
+
+    @Override
+    public void putMap() {
+        putElement(new MapElement(parent, current));
+    }
+
+    @Override
+    public void putArray(boolean described, DataType type) {
+        putElement(new ArrayElement(parent, current, described, type));
+    }
+
+    @Override
+    public void putDescribed() {
+        putElement(new DescribedTypeElement(parent, current));
+    }
+
+    @Override
+    public void putNull() {
+        putElement(new NullElement(parent, current));
+    }
+
+    @Override
+    public void putBoolean(boolean b) {
+        putElement(new BooleanElement(parent, current, b));
+    }
+
+    @Override
+    public void putUnsignedByte(UnsignedByte ub) {
+        putElement(new UnsignedByteElement(parent, current, ub));
+    }
+
+    @Override
+    public void putByte(byte b) {
+        putElement(new ByteElement(parent, current, b));
+    }
+
+    @Override
+    public void putUnsignedShort(UnsignedShort us) {
+        putElement(new UnsignedShortElement(parent, current, us));
+    }
+
+    @Override
+    public void putShort(short s) {
+        putElement(new ShortElement(parent, current, s));
+    }
+
+    @Override
+    public void putUnsignedInteger(UnsignedInteger ui) {
+        putElement(new UnsignedIntegerElement(parent, current, ui));
+    }
+
+    @Override
+    public void putInt(int i) {
+        putElement(new IntegerElement(parent, current, i));
+    }
+
+    @Override
+    public void putChar(int c) {
+        putElement(new CharElement(parent, current, c));
+    }
+
+    @Override
+    public void putUnsignedLong(UnsignedLong ul) {
+        putElement(new UnsignedLongElement(parent, current, ul));
+    }
+
+    @Override
+    public void putLong(long l) {
+        putElement(new LongElement(parent, current, l));
+    }
+
+    @Override
+    public void putTimestamp(Date t) {
+        putElement(new TimestampElement(parent, current, t));
+    }
+
+    @Override
+    public void putFloat(float f) {
+        putElement(new FloatElement(parent, current, f));
+    }
+
+    @Override
+    public void putDouble(double d) {
+        putElement(new DoubleElement(parent, current, d));
+    }
+
+    @Override
+    public void putDecimal32(Decimal32 d) {
+        putElement(new Decimal32Element(parent, current, d));
+    }
+
+    @Override
+    public void putDecimal64(Decimal64 d) {
+        putElement(new Decimal64Element(parent, current, d));
+    }
+
+    @Override
+    public void putDecimal128(Decimal128 d) {
+        putElement(new Decimal128Element(parent, current, d));
+    }
+
+    @Override
+    public void putUUID(UUID u) {
+        putElement(new UUIDElement(parent, current, u));
+    }
+
+    @Override
+    public void putBinary(Binary bytes) {
+        putElement(new BinaryElement(parent, current, bytes));
+    }
+
+    @Override
+    public void putBinary(byte[] bytes) {
+        putBinary(new Binary(bytes));
+    }
+
+    @Override
+    public void putString(String string) {
+        putElement(new StringElement(parent, current, string));
+    }
+
+    @Override
+    public void putSymbol(Symbol symbol) {
+        putElement(new SymbolElement(parent, current, symbol));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void putObject(Object o) {
+        if (o == null) {
+            putNull();
+        } else if (o instanceof Boolean) {
+            putBoolean((Boolean) o);
+        } else if (o instanceof UnsignedByte) {
+            putUnsignedByte((UnsignedByte) o);
+        } else if (o instanceof Byte) {
+            putByte((Byte) o);
+        } else if (o instanceof UnsignedShort) {
+            putUnsignedShort((UnsignedShort) o);
+        } else if (o instanceof Short) {
+            putShort((Short) o);
+        } else if (o instanceof UnsignedInteger) {
+            putUnsignedInteger((UnsignedInteger) o);
+        } else if (o instanceof Integer) {
+            putInt((Integer) o);
+        } else if (o instanceof Character) {
+            putChar((Character) o);
+        } else if (o instanceof UnsignedLong) {
+            putUnsignedLong((UnsignedLong) o);
+        } else if (o instanceof Long) {
+            putLong((Long) o);
+        } else if (o instanceof Date) {
+            putTimestamp((Date) o);
+        } else if (o instanceof Float) {
+            putFloat((Float) o);
+        } else if (o instanceof Double) {
+            putDouble((Double) o);
+        } else if (o instanceof Decimal32) {
+            putDecimal32((Decimal32) o);
+        } else if (o instanceof Decimal64) {
+            putDecimal64((Decimal64) o);
+        } else if (o instanceof Decimal128) {
+            putDecimal128((Decimal128) o);
+        } else if (o instanceof UUID) {
+            putUUID((UUID) o);
+        } else if (o instanceof Binary) {
+            putBinary((Binary) o);
+        } else if (o instanceof String) {
+            putString((String) o);
+        } else if (o instanceof Symbol) {
+            putSymbol((Symbol) o);
+        } else if (o instanceof DescribedType) {
+            putDescribedType((DescribedType) o);
+        } else if (o instanceof Symbol[]) {
+            putArray(false, Codec.DataType.SYMBOL);
+            enter();
+            for (Symbol s : (Symbol[]) o) {
+                putSymbol(s);
+            }
+            exit();
+        } else if (o instanceof Object[]) {
+            throw new IllegalArgumentException("Unsupported array type");
+        } else if (o instanceof List) {
+            putJavaList((List<Object>) o);
+        } else if (o instanceof Map) {
+            putJavaMap((Map<Object, Object>) o);
+        } else {
+            throw new IllegalArgumentException("Unknown type " + o.getClass().getSimpleName());
+        }
+    }
+
+    @Override
+    public void putJavaMap(Map<Object, Object> map) {
+        putMap();
+        enter();
+        for (Map.Entry<Object, Object> entry : map.entrySet()) {
+            putObject(entry.getKey());
+            putObject(entry.getValue());
+        }
+        exit();
+    }
+
+    @Override
+    public void putJavaList(List<Object> list) {
+        putList();
+        enter();
+        for (Object o : list) {
+            putObject(o);
+        }
+        exit();
+    }
+
+    @Override
+    public void putDescribedType(DescribedType dt) {
+        putElement(new DescribedTypeElement(parent, current));
+        enter();
+        putObject(dt.getDescriptor());
+        putObject(dt.getDescribed());
+        exit();
+    }
+
+    @Override
+    public long getList() {
+        if (current instanceof ListElement) {
+            return ((ListElement) current).count();
+        }
+        throw new IllegalStateException("Current value not list");
+    }
+
+    @Override
+    public long getMap() {
+        if (current instanceof MapElement) {
+            return ((MapElement) current).count();
+        }
+        throw new IllegalStateException("Current value not map");
+    }
+
+    @Override
+    public long getArray() {
+        if (current instanceof ArrayElement) {
+            return ((ArrayElement) current).count();
+        }
+        throw new IllegalStateException("Current value not array");
+    }
+
+    @Override
+    public boolean isArrayDescribed() {
+        if (current instanceof ArrayElement) {
+            return ((ArrayElement) current).isDescribed();
+        }
+        throw new IllegalStateException("Current value not array");
+    }
+
+    @Override
+    public DataType getArrayType() {
+        if (current instanceof ArrayElement) {
+            return ((ArrayElement) current).getArrayDataType();
+        }
+        throw new IllegalStateException("Current value not array");
+    }
+
+    @Override
+    public boolean isDescribed() {
+        return current != null && current.getDataType() == DataType.DESCRIBED;
+    }
+
+    @Override
+    public boolean isNull() {
+        return current != null && current.getDataType() == DataType.NULL;
+    }
+
+    @Override
+    public boolean getBoolean() {
+        if (current instanceof BooleanElement) {
+            return ((BooleanElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not boolean");
+    }
+
+    @Override
+    public UnsignedByte getUnsignedByte() {
+        if (current instanceof UnsignedByteElement) {
+            return ((UnsignedByteElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not unsigned byte");
+    }
+
+    @Override
+    public byte getByte() {
+        if (current instanceof ByteElement) {
+            return ((ByteElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not byte");
+    }
+
+    @Override
+    public UnsignedShort getUnsignedShort() {
+        if (current instanceof UnsignedShortElement) {
+            return ((UnsignedShortElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not unsigned short");
+    }
+
+    @Override
+    public short getShort() {
+        if (current instanceof ShortElement) {
+            return ((ShortElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not short");
+    }
+
+    @Override
+    public UnsignedInteger getUnsignedInteger() {
+        if (current instanceof UnsignedIntegerElement) {
+            return ((UnsignedIntegerElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not unsigned integer");
+    }
+
+    @Override
+    public int getInt() {
+        if (current instanceof IntegerElement) {
+            return ((IntegerElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not integer");
+    }
+
+    @Override
+    public int getChar() {
+        if (current instanceof CharElement) {
+            return ((CharElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not char");
+    }
+
+    @Override
+    public UnsignedLong getUnsignedLong() {
+        if (current instanceof UnsignedLongElement) {
+            return ((UnsignedLongElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not unsigned long");
+    }
+
+    @Override
+    public long getLong() {
+        if (current instanceof LongElement) {
+            return ((LongElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not long");
+    }
+
+    @Override
+    public Date getTimestamp() {
+        if (current instanceof TimestampElement) {
+            return ((TimestampElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not timestamp");
+    }
+
+    @Override
+    public float getFloat() {
+        if (current instanceof FloatElement) {
+            return ((FloatElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not float");
+    }
+
+    @Override
+    public double getDouble() {
+        if (current instanceof DoubleElement) {
+            return ((DoubleElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not double");
+    }
+
+    @Override
+    public Decimal32 getDecimal32() {
+        if (current instanceof Decimal32Element) {
+            return ((Decimal32Element) current).getValue();
+        }
+        throw new IllegalStateException("Current value not decimal32");
+    }
+
+    @Override
+    public Decimal64 getDecimal64() {
+        if (current instanceof Decimal64Element) {
+            return ((Decimal64Element) current).getValue();
+        }
+        throw new IllegalStateException("Current value not decimal32");
+    }
+
+    @Override
+    public Decimal128 getDecimal128() {
+        if (current instanceof Decimal128Element) {
+            return ((Decimal128Element) current).getValue();
+        }
+        throw new IllegalStateException("Current value not decimal32");
+    }
+
+    @Override
+    public UUID getUUID() {
+        if (current instanceof UUIDElement) {
+            return ((UUIDElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not uuid");
+    }
+
+    @Override
+    public Binary getBinary() {
+        if (current instanceof BinaryElement) {
+            return ((BinaryElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not binary");
+    }
+
+    @Override
+    public String getString() {
+        if (current instanceof StringElement) {
+            return ((StringElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not string");
+    }
+
+    @Override
+    public Symbol getSymbol() {
+        if (current instanceof SymbolElement) {
+            return ((SymbolElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not symbol");
+    }
+
+    @Override
+    public Object getObject() {
+        return current == null ? null : current.getValue();
+    }
+
+    @Override
+    public Map<Object, Object> getJavaMap() {
+        if (current instanceof MapElement) {
+            return ((MapElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not map");
+    }
+
+    @Override
+    public List<Object> getJavaList() {
+        if (current instanceof ListElement) {
+            return ((ListElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not list");
+    }
+
+    @Override
+    public Object[] getJavaArray() {
+        if (current instanceof ArrayElement) {
+            return ((ArrayElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not array");
+    }
+
+    @Override
+    public DescribedType getDescribedType() {
+        if (current instanceof DescribedTypeElement) {
+            return ((DescribedTypeElement) current).getValue();
+        }
+        throw new IllegalStateException("Current value not described type");
+    }
+
+    @Override
+    public String format() {
+        StringBuilder sb = new StringBuilder();
+        Element<?> el = first;
+        boolean first = true;
+        while (el != null) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(", ");
+            }
+            el.render(sb);
+            el = el.next();
+        }
+
+        return sb.toString();
+    }
+
+    private void render(StringBuilder sb, Element<?> el) {
+        if (el == null) {
+            return;
+        }
+
+        sb.append("    ").append(el).append("\n");
+        if (el.canEnter()) {
+            render(sb, el.child());
+        }
+        render(sb, el.next());
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        render(sb, first);
+        return String.format("Data[current=%h, parent=%h]{%n%s}", System.identityHashCode(current), System.identityHashCode(parent), sb);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal128Element.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal128Element.java
new file mode 100644
index 0000000..b2818ce
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal128Element.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal128;
+
+import io.netty.buffer.ByteBuf;
+
+class Decimal128Element extends AtomicElement<Decimal128> {
+
+    private final Decimal128 value;
+
+    Decimal128Element(Element<?> parent, Element<?> prev, Decimal128 d) {
+        super(parent, prev);
+        value = d;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 16 : 17;
+    }
+
+    @Override
+    public Decimal128 getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.DECIMAL128;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() >= size) {
+            if (size == 17) {
+                buffer.writeByte((byte) 0x94);
+            }
+            buffer.writeLong(value.getMostSignificantBits());
+            buffer.writeLong(value.getLeastSignificantBits());
+            return size;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal32Element.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal32Element.java
new file mode 100644
index 0000000..9e2dd5a
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal32Element.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal32;
+
+import io.netty.buffer.ByteBuf;
+
+class Decimal32Element extends AtomicElement<Decimal32> {
+
+    private final Decimal32 value;
+
+    Decimal32Element(Element<?> parent, Element<?> prev, Decimal32 d) {
+        super(parent, prev);
+        value = d;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 4 : 5;
+    }
+
+    @Override
+    public Decimal32 getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.DECIMAL32;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() >= size) {
+            if (size == 5) {
+                buffer.writeByte((byte) 0x74);
+            }
+            buffer.writeInt(value.getBits());
+            return size;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal64Element.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal64Element.java
new file mode 100644
index 0000000..0de8375
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Decimal64Element.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal64;
+
+import io.netty.buffer.ByteBuf;
+
+class Decimal64Element extends AtomicElement<Decimal64> {
+
+    private final Decimal64 value;
+
+    Decimal64Element(Element<?> parent, Element<?> prev, Decimal64 d) {
+        super(parent, prev);
+        value = d;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 8 : 9;
+    }
+
+    @Override
+    public Decimal64 getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.DECIMAL64;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() >= size) {
+            if (size == 9) {
+                buffer.writeByte((byte) 0x84);
+            }
+            buffer.writeLong(value.getBits());
+            return size;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeElement.java
new file mode 100644
index 0000000..847b6eb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeElement.java
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+import io.netty.buffer.ByteBuf;
+
+class DescribedTypeElement extends AbstractElement<DescribedType> {
+
+    private Element<?> first;
+
+    DescribedTypeElement(Element<?> parent, Element<?> prev) {
+        super(parent, prev);
+    }
+
+    @Override
+    public int size() {
+        int count = 0;
+        int size = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            size += elt.size();
+            elt = elt.next();
+        }
+
+        if (isElementOfArray()) {
+            throw new IllegalArgumentException("Cannot add described type members to an array");
+        } else if (count > 2) {
+            throw new IllegalArgumentException("Too many elements in described type");
+        } else if (count == 0) {
+            size = 3;
+        } else if (count == 1) {
+            size += 2;
+        } else {
+            size += 1;
+        }
+
+        return size;
+    }
+
+    @Override
+    public DescribedType getValue() {
+        final Object descriptor = first == null ? null : first.getValue();
+        Element<?> second = first == null ? null : first.next();
+        final Object described = second == null ? null : second.getValue();
+        return DescribedTypeRegistry.lookupDescribedType(descriptor, described);
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.DESCRIBED;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int encodedSize = size();
+
+        if (encodedSize > buffer.maxWritableBytes()) {
+            return 0;
+        } else {
+            buffer.writeByte((byte) 0);
+            if (first == null) {
+                buffer.writeByte((byte) 0x40);
+                buffer.writeByte((byte) 0x40);
+            } else {
+                first.encode(buffer);
+                if (first.next() == null) {
+                    buffer.writeByte((byte) 0x40);
+                } else {
+                    first.next().encode(buffer);
+                }
+            }
+        }
+        return encodedSize;
+    }
+
+    @Override
+    public boolean canEnter() {
+        return true;
+    }
+
+    @Override
+    public Element<?> child() {
+        return first;
+    }
+
+    @Override
+    public void setChild(Element<?> elt) {
+        first = elt;
+    }
+
+    @Override
+    public Element<?> checkChild(Element<?> element) {
+        if (element.prev() != first) {
+            throw new IllegalArgumentException("Described Type may only have two elements");
+        }
+        return element;
+
+    }
+
+    @Override
+    public Element<?> addChild(Element<?> element) {
+        first = element;
+        return element;
+    }
+
+    @Override
+    String startSymbol() {
+        return "(";
+    }
+
+    @Override
+    String stopSymbol() {
+        return ")";
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeImpl.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeImpl.java
new file mode 100644
index 0000000..4c2fdd6
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeImpl.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+class DescribedTypeImpl implements DescribedType {
+
+    private final Object descriptor;
+    private final Object described;
+
+    public DescribedTypeImpl(final Object descriptor, final Object described) {
+        this.descriptor = descriptor;
+        this.described = described;
+    }
+
+    @Override
+    public Object getDescriptor() {
+        return descriptor;
+    }
+
+    @Override
+    public Object getDescribed() {
+        return described;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || !(o instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType that = (DescribedType) o;
+
+        if (described != null ? !described.equals(that.getDescribed()) : that.getDescribed() != null) {
+            return false;
+        }
+        if (descriptor != null ? !descriptor.equals(that.getDescriptor()) : that.getDescriptor() != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = descriptor != null ? descriptor.hashCode() : 0;
+        result = 31 * result + (described != null ? described.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "{" + descriptor + ": " + described + '}';
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeRegistry.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeRegistry.java
new file mode 100644
index 0000000..fb9f220
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DescribedTypeRegistry.java
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpValue;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Data;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnClose;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnNoLinks;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnNoLinksOrMessages;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnNoMessages;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Footer;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Header;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Properties;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Received;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Rejected;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslChallenge;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslInit;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslMechanisms;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslResponse;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declare;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declared;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Discharge;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.TransactionalState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Close;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+/**
+ * Registry of described types know to the Data type codec
+ */
+public abstract class DescribedTypeRegistry {
+
+    private static Map<Object, Class<? extends DescribedType>> describedTypes = new HashMap<>();
+
+    static {
+        describedTypes.put(Accepted.DESCRIPTOR_CODE, Accepted.class);
+        describedTypes.put(Accepted.DESCRIPTOR_SYMBOL, Accepted.class);
+        describedTypes.put(Attach.DESCRIPTOR_CODE, Attach.class);
+        describedTypes.put(Attach.DESCRIPTOR_SYMBOL, Attach.class);
+        describedTypes.put(Begin.DESCRIPTOR_CODE, Begin.class);
+        describedTypes.put(Begin.DESCRIPTOR_SYMBOL, Begin.class);
+        describedTypes.put(Close.DESCRIPTOR_CODE, Close.class);
+        describedTypes.put(Close.DESCRIPTOR_SYMBOL, Close.class);
+        describedTypes.put(Coordinator.DESCRIPTOR_CODE, Coordinator.class);
+        describedTypes.put(Coordinator.DESCRIPTOR_SYMBOL, Coordinator.class);
+        describedTypes.put(Declare.DESCRIPTOR_CODE, Declare.class);
+        describedTypes.put(Declare.DESCRIPTOR_SYMBOL, Declare.class);
+        describedTypes.put(Declared.DESCRIPTOR_CODE, Declared.class);
+        describedTypes.put(Declared.DESCRIPTOR_SYMBOL, Declared.class);
+        describedTypes.put(DeleteOnClose.DESCRIPTOR_CODE, DeleteOnClose.class);
+        describedTypes.put(DeleteOnClose.DESCRIPTOR_SYMBOL, DeleteOnClose.class);
+        describedTypes.put(DeleteOnNoLinks.DESCRIPTOR_CODE, DeleteOnNoLinks.class);
+        describedTypes.put(DeleteOnNoLinks.DESCRIPTOR_SYMBOL, DeleteOnNoLinks.class);
+        describedTypes.put(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE, DeleteOnNoLinksOrMessages.class);
+        describedTypes.put(DeleteOnNoLinksOrMessages.DESCRIPTOR_SYMBOL, DeleteOnNoLinksOrMessages.class);
+        describedTypes.put(DeleteOnNoMessages.DESCRIPTOR_CODE, DeleteOnNoMessages.class);
+        describedTypes.put(DeleteOnNoMessages.DESCRIPTOR_SYMBOL, DeleteOnNoMessages.class);
+        describedTypes.put(Detach.DESCRIPTOR_CODE, Detach.class);
+        describedTypes.put(Detach.DESCRIPTOR_SYMBOL, Detach.class);
+        describedTypes.put(Discharge.DESCRIPTOR_CODE, Discharge.class);
+        describedTypes.put(Discharge.DESCRIPTOR_SYMBOL, Discharge.class);
+        describedTypes.put(Disposition.DESCRIPTOR_CODE, Disposition.class);
+        describedTypes.put(Disposition.DESCRIPTOR_SYMBOL, Disposition.class);
+        describedTypes.put(End.DESCRIPTOR_CODE, End.class);
+        describedTypes.put(End.DESCRIPTOR_SYMBOL, End.class);
+        describedTypes.put(ErrorCondition.DESCRIPTOR_CODE, ErrorCondition.class);
+        describedTypes.put(ErrorCondition.DESCRIPTOR_SYMBOL, ErrorCondition.class);
+        describedTypes.put(Flow.DESCRIPTOR_CODE, Flow.class);
+        describedTypes.put(Flow.DESCRIPTOR_SYMBOL, Flow.class);
+        describedTypes.put(Modified.DESCRIPTOR_CODE, Modified.class);
+        describedTypes.put(Modified.DESCRIPTOR_SYMBOL, Modified.class);
+        describedTypes.put(Open.DESCRIPTOR_CODE, Open.class);
+        describedTypes.put(Open.DESCRIPTOR_SYMBOL, Open.class);
+        describedTypes.put(Received.DESCRIPTOR_CODE, Received.class);
+        describedTypes.put(Received.DESCRIPTOR_SYMBOL, Received.class);
+        describedTypes.put(Rejected.DESCRIPTOR_CODE, Rejected.class);
+        describedTypes.put(Rejected.DESCRIPTOR_SYMBOL, Rejected.class);
+        describedTypes.put(Released.DESCRIPTOR_CODE, Released.class);
+        describedTypes.put(Released.DESCRIPTOR_SYMBOL, Released.class);
+        describedTypes.put(SaslChallenge.DESCRIPTOR_CODE, SaslChallenge.class);
+        describedTypes.put(SaslChallenge.DESCRIPTOR_SYMBOL, SaslChallenge.class);
+        describedTypes.put(SaslInit.DESCRIPTOR_CODE, SaslInit.class);
+        describedTypes.put(SaslInit.DESCRIPTOR_SYMBOL, SaslInit.class);
+        describedTypes.put(SaslMechanisms.DESCRIPTOR_CODE, SaslMechanisms.class);
+        describedTypes.put(SaslMechanisms.DESCRIPTOR_SYMBOL, SaslMechanisms.class);
+        describedTypes.put(SaslOutcome.DESCRIPTOR_CODE, SaslOutcome.class);
+        describedTypes.put(SaslOutcome.DESCRIPTOR_SYMBOL, SaslOutcome.class);
+        describedTypes.put(SaslResponse.DESCRIPTOR_CODE, SaslResponse.class);
+        describedTypes.put(SaslResponse.DESCRIPTOR_SYMBOL, SaslResponse.class);
+        describedTypes.put(Source.DESCRIPTOR_CODE, Source.class);
+        describedTypes.put(Source.DESCRIPTOR_SYMBOL, Source.class);
+        describedTypes.put(Target.DESCRIPTOR_CODE, Target.class);
+        describedTypes.put(Target.DESCRIPTOR_SYMBOL, Target.class);
+        describedTypes.put(TransactionalState.DESCRIPTOR_CODE, TransactionalState.class);
+        describedTypes.put(TransactionalState.DESCRIPTOR_SYMBOL, TransactionalState.class);
+        describedTypes.put(Transfer.DESCRIPTOR_CODE, Transfer.class);
+        describedTypes.put(Transfer.DESCRIPTOR_SYMBOL, Transfer.class);
+        describedTypes.put(AmqpSequence.DESCRIPTOR_CODE, AmqpSequence.class);
+        describedTypes.put(AmqpSequence.DESCRIPTOR_SYMBOL, AmqpSequence.class);
+        describedTypes.put(AmqpValue.DESCRIPTOR_CODE, AmqpValue.class);
+        describedTypes.put(AmqpValue.DESCRIPTOR_SYMBOL, AmqpValue.class);
+        describedTypes.put(ApplicationProperties.DESCRIPTOR_CODE, ApplicationProperties.class);
+        describedTypes.put(ApplicationProperties.DESCRIPTOR_SYMBOL, ApplicationProperties.class);
+        describedTypes.put(Data.DESCRIPTOR_CODE, Data.class);
+        describedTypes.put(Data.DESCRIPTOR_SYMBOL, Data.class);
+        describedTypes.put(DeliveryAnnotations.DESCRIPTOR_CODE, DeliveryAnnotations.class);
+        describedTypes.put(DeliveryAnnotations.DESCRIPTOR_SYMBOL, DeliveryAnnotations.class);
+        describedTypes.put(Footer.DESCRIPTOR_CODE, Footer.class);
+        describedTypes.put(Footer.DESCRIPTOR_SYMBOL, Footer.class);
+        describedTypes.put(Header.DESCRIPTOR_CODE, Header.class);
+        describedTypes.put(Header.DESCRIPTOR_SYMBOL, Header.class);
+        describedTypes.put(MessageAnnotations.DESCRIPTOR_CODE, MessageAnnotations.class);
+        describedTypes.put(MessageAnnotations.DESCRIPTOR_SYMBOL, MessageAnnotations.class);
+        describedTypes.put(Properties.DESCRIPTOR_CODE, Properties.class);
+        describedTypes.put(Properties.DESCRIPTOR_SYMBOL, Properties.class);
+    }
+
+    private DescribedTypeRegistry() {
+    }
+
+    static DescribedType lookupDescribedType(Object descriptor, Object described) {
+        Class<? extends DescribedType> typeClass = describedTypes.get(descriptor);
+        if (typeClass != null) {
+            try {
+                Constructor<? extends DescribedType> constructor = typeClass.getConstructor(Object.class);
+                return constructor.newInstance(described);
+            } catch (Throwable err){
+            }
+        }
+
+        return new DescribedTypeImpl(descriptor, described);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DoubleElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DoubleElement.java
new file mode 100644
index 0000000..8fce689
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/DoubleElement.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class DoubleElement extends AtomicElement<Double> {
+
+    private final double value;
+
+    DoubleElement(Element<?> parent, Element<?> prev, double d) {
+        super(parent, prev);
+        value = d;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 8 : 9;
+    }
+
+    @Override
+    public Double getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.DOUBLE;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() >= size) {
+            if (size == 9) {
+                buffer.writeByte((byte) 0x82);
+            }
+            buffer.writeDouble(value);
+            return size;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Element.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Element.java
new file mode 100644
index 0000000..b2b7f03
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/Element.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+interface Element<T> {
+
+    int size();
+
+    T getValue();
+
+    Codec.DataType getDataType();
+
+    int encode(ByteBuf buffer);
+
+    Element<?> next();
+
+    Element<?> prev();
+
+    Element<?> child();
+
+    Element<?> parent();
+
+    void setNext(Element<?> elt);
+
+    void setPrev(Element<?> elt);
+
+    void setParent(Element<?> elt);
+
+    void setChild(Element<?> elt);
+
+    Element<?> replaceWith(Element<?> elt);
+
+    Element<?> addChild(Element<?> element);
+
+    Element<?> checkChild(Element<?> element);
+
+    boolean canEnter();
+
+    void render(StringBuilder sb);
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/EncodingCodes.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/EncodingCodes.java
new file mode 100644
index 0000000..fb9bda4
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/EncodingCodes.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+public interface EncodingCodes {
+
+    public static final byte DESCRIBED_TYPE_INDICATOR = (byte) 0x00;
+
+    public static final byte NULL                     = (byte) 0x40;
+
+    public static final byte BOOLEAN                  = (byte) 0x56;
+    public static final byte BOOLEAN_TRUE             = (byte) 0x41;
+    public static final byte BOOLEAN_FALSE            = (byte) 0x42;
+
+    public static final byte UBYTE                    = (byte) 0x50;
+
+    public static final byte USHORT                   = (byte) 0x60;
+
+    public static final byte UINT                     = (byte) 0x70;
+    public static final byte SMALLUINT                = (byte) 0x52;
+    public static final byte UINT0                    = (byte) 0x43;
+
+    public static final byte ULONG                    = (byte) 0x80;
+    public static final byte SMALLULONG               = (byte) 0x53;
+    public static final byte ULONG0                   = (byte) 0x44;
+
+    public static final byte BYTE                     = (byte) 0x51;
+
+    public static final byte SHORT                    = (byte) 0x61;
+
+    public static final byte INT                      = (byte) 0x71;
+    public static final byte SMALLINT                 = (byte) 0x54;
+
+    public static final byte LONG                     = (byte) 0x81;
+    public static final byte SMALLLONG                = (byte) 0x55;
+
+    public static final byte FLOAT                    = (byte) 0x72;
+
+    public static final byte DOUBLE                   = (byte) 0x82;
+
+    public static final byte DECIMAL32                = (byte) 0x74;
+
+    public static final byte DECIMAL64                = (byte) 0x84;
+
+    public static final byte DECIMAL128               = (byte) 0x94;
+
+    public static final byte CHAR                     = (byte) 0x73;
+
+    public static final byte TIMESTAMP                = (byte) 0x83;
+
+    public static final byte UUID                     = (byte) 0x98;
+
+    public static final byte VBIN8                    = (byte) 0xa0;
+    public static final byte VBIN32                   = (byte) 0xb0;
+
+    public static final byte STR8                     = (byte) 0xa1;
+    public static final byte STR32                    = (byte) 0xb1;
+
+    public static final byte SYM8                     = (byte) 0xa3;
+    public static final byte SYM32                    = (byte) 0xb3;
+
+    public static final byte LIST0                    = (byte) 0x45;
+    public static final byte LIST8                    = (byte) 0xc0;
+    public static final byte LIST32                   = (byte) 0xd0;
+
+    public static final byte MAP8                     = (byte) 0xc1;
+    public static final byte MAP32                    = (byte) 0xd1;
+
+    public static final byte ARRAY8                   = (byte) 0xe0;
+    public static final byte ARRAY32                  = (byte) 0xf0;
+
+    static String toString(byte encoding) {
+        switch (encoding) {
+            case DESCRIBED_TYPE_INDICATOR:
+                return "DESCRIBED_TYPE_INDICATOR:0x00";
+            case NULL:
+                return "NULL:0x40";
+            case BOOLEAN:
+                return "BOOLEAN:0x56";
+            case BOOLEAN_TRUE:
+                return "BOOLEAN_TRUE:0x41";
+            case BOOLEAN_FALSE:
+                return "BOOLEAN_FALSE:0x42";
+            case UBYTE:
+                return "UBYTE:0x50";
+            case USHORT:
+                return "USHORT:0x60";
+            case UINT:
+                return "UINT:0x70";
+            case SMALLUINT:
+                return "SMALLUINT:0x52";
+            case UINT0:
+                return "UINT0:0x43";
+            case ULONG:
+                return "ULONG:0x80";
+            case SMALLULONG:
+                return "SMALLULONG:0x53";
+            case ULONG0:
+                return "ULONG0:0x44";
+            case BYTE:
+                return "BYTE:0x51";
+            case SHORT:
+                return "SHORT:0x61";
+            case INT:
+                return "INT:0x71";
+            case SMALLINT:
+                return "SMALLINT:0x54";
+            case LONG:
+                return "LONG:0x81";
+            case SMALLLONG:
+                return "SMALLLONG:0x55";
+            case FLOAT:
+                return "FLOAT:0x72";
+            case DOUBLE:
+                return "DOUBLE:0x82";
+            case DECIMAL32:
+                return "DECIMAL32:0x74";
+            case DECIMAL64:
+                return "DECIMAL64:0x84";
+            case DECIMAL128:
+                return "DECIMAL128:0x94";
+            case CHAR:
+                return "CHAR:0x73";
+            case TIMESTAMP:
+                return "TIMESTAMP:0x83";
+            case UUID:
+                return "UUID:0x98";
+            case VBIN8:
+                return "VBIN8:0xa0";
+            case VBIN32:
+                return "VBIN32:0xb0";
+            case STR8:
+                return "STR8:0xa1";
+            case STR32:
+                return "STR32:0xb1";
+            case SYM8:
+                return "SYM8:0xa3";
+            case SYM32:
+                return "SYM32:0xb3";
+            case LIST0:
+                return "LIST0:0x45";
+            case LIST8:
+                return "LIST8:0xc0";
+            case LIST32:
+                return "LIST32:0xd0";
+            case MAP8:
+                return "MAP8:0xc1";
+            case MAP32:
+                return "MAP32:0xd1";
+            case ARRAY8:
+                return "ARRAY32:0xe0";
+            case ARRAY32:
+                return "ARRAY32:0xf0";
+            default:
+                return "Unknown-Type:" + String.format("0x%02X ", encoding);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/FloatElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/FloatElement.java
new file mode 100644
index 0000000..36a61ea
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/FloatElement.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class FloatElement extends AtomicElement<Float> {
+
+    private final float value;
+
+    FloatElement(Element<?> parent, Element<?> prev, float f) {
+        super(parent, prev);
+        value = f;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 4 : 5;
+    }
+
+    @Override
+    public Float getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.FLOAT;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() >= size) {
+            if (size == 5) {
+                buffer.writeByte((byte) 0x72);
+            }
+            buffer.writeFloat(value);
+            return size;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/IntegerElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/IntegerElement.java
new file mode 100644
index 0000000..3629543
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/IntegerElement.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class IntegerElement extends AtomicElement<Integer> {
+
+    private final int value;
+
+    IntegerElement(Element<?> parent, Element<?> prev, int i) {
+        super(parent, prev);
+        value = i;
+    }
+
+    @Override
+    public int size() {
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (-128 <= value && value <= 127) {
+                    return 1;
+                } else {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                    return 4;
+                }
+            } else {
+                return 4;
+            }
+        } else {
+            return (-128 <= value && value <= 127) ? 2 : 5;
+        }
+    }
+
+    @Override
+    public Integer getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.INT;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (size <= buffer.maxWritableBytes()) {
+            switch (size) {
+                case 2:
+                    buffer.writeByte((byte) 0x54);
+                case 1:
+                    buffer.writeByte((byte) value);
+                    break;
+
+                case 5:
+                    buffer.writeByte((byte) 0x71);
+                case 4:
+                    buffer.writeInt(value);
+
+            }
+
+            return size;
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ListDescribedType.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ListDescribedType.java
new file mode 100644
index 0000000..f134c44
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ListDescribedType.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+public abstract class ListDescribedType implements DescribedType {
+
+    private final ArrayList<Object> fields;
+
+    public ListDescribedType(int numberOfFields) {
+        fields = new ArrayList<>(numberOfFields);
+
+        for (int i = 0; i < numberOfFields; ++i) {
+            fields.add(null);
+        }
+    }
+
+    public ListDescribedType(int numberOfFields, ListDescribedType described) {
+        if (described.fields.size() > numberOfFields) {
+            throw new IllegalArgumentException("List encoded exceeds expected number of elements for this type");
+        }
+
+        fields = new ArrayList<>(numberOfFields);
+
+        for (int i = 0; i < numberOfFields; ++i) {
+            if (i < described.fields.size()) {
+                fields.add(described.fields.get(i));
+            } else {
+                fields.add(null);
+            }
+        }
+    }
+
+    public ListDescribedType(int numberOfFields, List<Object> described) {
+        if (described.size() > numberOfFields) {
+            throw new IllegalArgumentException("List encoded exceeds expected number of elements for this type");
+        }
+
+        fields = new ArrayList<>(numberOfFields);
+
+        for (int i = 0; i < numberOfFields; ++i) {
+            if (i < described.size()) {
+                fields.add(described.get(i));
+            } else {
+                fields.add(null);
+            }
+        }
+    }
+
+    @Override
+    public List<Object> getDescribed() {
+        // Return a List containing only the 'used fields' (i.e up to the
+        // highest field used)
+        int highestSetFeild = -1;
+        for (int i = 0; i < fields.size(); ++i) {
+            if (fields.get(i) != null) {
+                highestSetFeild = i;
+            }
+        }
+
+        // Create a list with the fields in the correct positions.
+        List<Object> list = new ArrayList<>();
+        for (int j = 0; j <= highestSetFeild; j++) {
+            list.add(fields.get(j));
+        }
+
+        return list;
+    }
+
+    public Object getFieldValue(int index) {
+        if (index < fields.size()) {
+            return fields.get(index);
+        } else {
+            throw new AssertionError("Request for unknown field in type: " + this);
+        }
+    }
+
+    protected int getHighestSetFieldId() {
+        int numUsedFields = 0;
+        for (Object element : fields) {
+            if (element != null) {
+                numUsedFields++;
+            }
+        }
+
+        return numUsedFields;
+    }
+
+    protected ArrayList<Object> getList() {
+        return fields;
+    }
+
+    protected Object[] getFields() {
+        return fields.toArray();
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " [descriptor=" + getDescriptor() + " fields=" + Arrays.toString(getFields()) + "]";
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ListElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ListElement.java
new file mode 100644
index 0000000..5a0eea7
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ListElement.java
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import io.netty.buffer.ByteBuf;
+
+class ListElement extends AbstractElement<List<Object>> {
+
+    private Element<?> first;
+
+    ListElement(Element<?> parent, Element<?> prev) {
+        super(parent, prev);
+    }
+
+    public int count() {
+        int count = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            elt = elt.next();
+        }
+        return count;
+    }
+
+    @Override
+    public int size() {
+        int count = 0;
+        int size = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            size += elt.size();
+            elt = elt.next();
+        }
+        if (isElementOfArray()) {
+            ArrayElement parent = (ArrayElement) parent();
+            if (parent.constructorType() == ArrayElement.TINY) {
+                if (count != 0) {
+                    parent.setConstructorType(ArrayElement.ConstructorType.SMALL);
+                    size += 2;
+                }
+            } else if (parent.constructorType() == ArrayElement.SMALL) {
+                if (count > 255 || size > 254) {
+                    parent.setConstructorType(ArrayElement.ConstructorType.LARGE);
+                    size += 8;
+                } else {
+                    size += 2;
+                }
+            } else {
+                size += 8;
+            }
+
+        } else {
+            if (count == 0) {
+                size = 1;
+            } else if (count <= 255 && size <= 254) {
+                size += 3;
+            } else {
+                size += 9;
+            }
+        }
+
+        return size;
+    }
+
+    @Override
+    public List<Object> getValue() {
+        List<Object> list = new ArrayList<>();
+        Element<?> elt = first;
+        while (elt != null) {
+            list.add(elt.getValue());
+            elt = elt.next();
+        }
+
+        return Collections.unmodifiableList(list);
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.LIST;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int encodedSize = size();
+
+        int count = 0;
+        int size = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            size += elt.size();
+            elt = elt.next();
+        }
+
+        if (encodedSize > buffer.maxWritableBytes()) {
+            return 0;
+        } else {
+            if (isElementOfArray()) {
+                switch (((ArrayElement) parent()).constructorType()) {
+                    case TINY:
+                        break;
+                    case SMALL:
+                        buffer.writeByte((byte) (size + 1));
+                        buffer.writeByte((byte) count);
+                        break;
+                    case LARGE:
+                        buffer.writeInt((size + 4));
+                        buffer.writeInt(count);
+                }
+            } else {
+                if (count == 0) {
+                    buffer.writeByte((byte) 0x45);
+                } else if (size <= 254 && count <= 255) {
+                    buffer.writeByte((byte) 0xc0);
+                    buffer.writeByte((byte) (size + 1));
+                    buffer.writeByte((byte) count);
+                } else {
+                    buffer.writeByte((byte) 0xd0);
+                    buffer.writeInt((size + 4));
+                    buffer.writeInt(count);
+                }
+
+            }
+
+            elt = first;
+            while (elt != null) {
+                elt.encode(buffer);
+                elt = elt.next();
+            }
+
+            return encodedSize;
+        }
+    }
+
+    @Override
+    public boolean canEnter() {
+        return true;
+    }
+
+    @Override
+    public Element<?> child() {
+        return first;
+    }
+
+    @Override
+    public void setChild(Element<?> elt) {
+        first = elt;
+    }
+
+    @Override
+    public Element<?> checkChild(Element<?> element) {
+        return element;
+    }
+
+    @Override
+    public Element<?> addChild(Element<?> element) {
+        first = element;
+        return element;
+    }
+
+    @Override
+    String startSymbol() {
+        return "[";
+    }
+
+    @Override
+    String stopSymbol() {
+        return "]";
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/LongElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/LongElement.java
new file mode 100644
index 0000000..563854e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/LongElement.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class LongElement extends AtomicElement<Long> {
+
+    private final long value;
+
+    LongElement(Element<?> parent, Element<?> prev, long l) {
+        super(parent, prev);
+        value = l;
+    }
+
+    @Override
+    public int size() {
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (-128l <= value && value <= 127l) {
+                    return 1;
+                } else {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                }
+            }
+
+            return 8;
+
+        } else {
+            return (-128l <= value && value <= 127l) ? 2 : 9;
+        }
+
+    }
+
+    @Override
+    public Long getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.LONG;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (size > buffer.maxWritableBytes()) {
+            return 0;
+        }
+        switch (size) {
+            case 2:
+                buffer.writeByte((byte) 0x55);
+            case 1:
+                buffer.writeByte((byte) value);
+                break;
+            case 9:
+                buffer.writeByte((byte) 0x81);
+            case 8:
+                buffer.writeLong(value);
+
+        }
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/MapDescribedType.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/MapDescribedType.java
new file mode 100644
index 0000000..1f3cd0b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/MapDescribedType.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+/**
+ * Basic Described type that contains a Map as the value.
+ */
+public abstract class MapDescribedType implements DescribedType {
+
+    private final Map<Object, Object> fields;
+
+    public MapDescribedType() {
+        fields = new HashMap<>();
+    }
+
+    @Override
+    public Map<Object, Object> getDescribed() {
+        return fields;
+    }
+
+    @Override
+    public String toString() {
+        return "MapDescribedType [descriptor=" + getDescriptor() + " fields=" + fields + "]";
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/MapElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/MapElement.java
new file mode 100644
index 0000000..0f5db68
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/MapElement.java
@@ -0,0 +1,190 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import io.netty.buffer.ByteBuf;
+
+class MapElement extends AbstractElement<Map<Object, Object>> {
+
+    private Element<?> first;
+
+    MapElement(Element<?> parent, Element<?> prev) {
+        super(parent, prev);
+    }
+
+    public int count() {
+        int count = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            elt = elt.next();
+        }
+        return count;
+    }
+
+    @Override
+    public int size() {
+        int count = 0;
+        int size = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            size += elt.size();
+            elt = elt.next();
+        }
+        if (isElementOfArray()) {
+            ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (count > 255 || size > 254) {
+                    parent.setConstructorType(ArrayElement.ConstructorType.LARGE);
+                    size += 8;
+                } else {
+                    size += 2;
+                }
+            } else {
+                size += 8;
+            }
+        } else {
+            if (count <= 255 && size <= 254) {
+                size += 3;
+            } else {
+                size += 9;
+            }
+        }
+
+        return size;
+    }
+
+    @Override
+    public Map<Object, Object> getValue() {
+        LinkedHashMap<Object, Object> map = new LinkedHashMap<>();
+        Element<?> elt = first;
+        while (elt != null) {
+            Object key = elt.getValue();
+            Object value;
+            elt = elt.next();
+            if (elt != null) {
+                value = elt.getValue();
+                elt = elt.next();
+            } else {
+                value = null;
+            }
+            map.put(key, value);
+        }
+
+        return Collections.unmodifiableMap(map);
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.MAP;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int encodedSize = size();
+
+        int count = 0;
+        int size = 0;
+        Element<?> elt = first;
+        while (elt != null) {
+            count++;
+            size += elt.size();
+            elt = elt.next();
+        }
+
+        if (encodedSize > buffer.maxWritableBytes()) {
+            return 0;
+        } else {
+            if (isElementOfArray()) {
+                switch (((ArrayElement) parent()).constructorType()) {
+                    case SMALL:
+                        buffer.writeByte((byte) (size + 1));
+                        buffer.writeByte((byte) count);
+                        break;
+                    case LARGE:
+                        buffer.writeInt((size + 4));
+                        buffer.writeInt(count);
+                    case TINY:
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                if (size <= 254 && count <= 255) {
+                    buffer.writeByte((byte) 0xc1);
+                    buffer.writeByte((byte) (size + 1));
+                    buffer.writeByte((byte) count);
+                } else {
+                    buffer.writeByte((byte) 0xd1);
+                    buffer.writeInt((size + 4));
+                    buffer.writeInt(count);
+                }
+
+            }
+
+            elt = first;
+            while (elt != null) {
+                elt.encode(buffer);
+                elt = elt.next();
+            }
+
+            return encodedSize;
+        }
+    }
+
+    @Override
+    public boolean canEnter() {
+        return true;
+    }
+
+    @Override
+    public Element<?> child() {
+        return first;
+    }
+
+    @Override
+    public void setChild(Element<?> elt) {
+        first = elt;
+    }
+
+    @Override
+    public Element<?> checkChild(Element<?> element) {
+        return element;
+    }
+
+    @Override
+    public Element<?> addChild(Element<?> element) {
+        first = element;
+        return element;
+    }
+
+    @Override
+    String startSymbol() {
+        return "{";
+    }
+
+    @Override
+    String stopSymbol() {
+        return "}";
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/NullElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/NullElement.java
new file mode 100644
index 0000000..557a3fd
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/NullElement.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class NullElement extends AtomicElement<Void> {
+
+    NullElement(Element<?> parent, Element<?> prev) {
+        super(parent, prev);
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 0 : 1;
+    }
+
+    @Override
+    public Void getValue() {
+        return null;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.NULL;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        if (buffer.isWritable() && !isElementOfArray()) {
+            buffer.writeByte((byte) 0x40);
+            return 1;
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ShortElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ShortElement.java
new file mode 100644
index 0000000..eb27af6
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/ShortElement.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import io.netty.buffer.ByteBuf;
+
+class ShortElement extends AtomicElement<Short> {
+
+    private final short value;
+
+    ShortElement(Element<?> parent, Element<?> prev, short s) {
+        super(parent, prev);
+        value = s;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 2 : 3;
+    }
+
+    @Override
+    public Short getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.SHORT;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        if (isElementOfArray()) {
+            if (buffer.maxWritableBytes() >= 2) {
+                buffer.writeShort(value);
+                return 2;
+            }
+        } else {
+            if (buffer.maxWritableBytes() >= 3) {
+                buffer.writeByte((byte) 0x61);
+                buffer.writeShort(value);
+                return 3;
+            }
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/StringElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/StringElement.java
new file mode 100644
index 0000000..41f4ffe
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/StringElement.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.nio.charset.Charset;
+
+import io.netty.buffer.ByteBuf;
+
+class StringElement extends AtomicElement<String> {
+
+    private static final Charset UTF_8 = Charset.forName("UTF-8");
+    private final String value;
+
+    StringElement(Element<?> parent, Element<?> prev, String s) {
+        super(parent, prev);
+        value = s;
+    }
+
+    @Override
+    public int size() {
+        final int length = value.getBytes(UTF_8).length;
+
+        return size(length);
+    }
+
+    private int size(int length) {
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (length > 255) {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                    return 4 + length;
+                } else {
+                    return 1 + length;
+                }
+            } else {
+                return 4 + length;
+            }
+        } else {
+            if (length > 255) {
+                return 5 + length;
+            } else {
+                return 2 + length;
+            }
+        }
+    }
+
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.STRING;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        final byte[] bytes = value.getBytes(UTF_8);
+        final int length = bytes.length;
+
+        int size = size(length);
+        if (buffer.maxWritableBytes() < size) {
+            return 0;
+        }
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                buffer.writeByte((byte) length);
+            } else {
+                buffer.writeInt(length);
+            }
+        } else if (length <= 255) {
+            buffer.writeByte((byte) 0xa1);
+            buffer.writeByte((byte) length);
+        } else {
+            buffer.writeByte((byte) 0xb1);
+            buffer.writeInt(length);
+        }
+        buffer.writeBytes(bytes);
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/SymbolElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/SymbolElement.java
new file mode 100644
index 0000000..9f914fb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/SymbolElement.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.nio.charset.Charset;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+
+import io.netty.buffer.ByteBuf;
+
+class SymbolElement extends AtomicElement<Symbol> {
+
+    private static final Charset ASCII = Charset.forName("US-ASCII");
+    private final Symbol value;
+
+    SymbolElement(Element<?> parent, Element<?> prev, Symbol s) {
+        super(parent, prev);
+        value = s;
+    }
+
+    @Override
+    public int size() {
+        final int length = value.getLength();
+
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (length > 255) {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                    return 4 + length;
+                } else {
+                    return 1 + length;
+                }
+            } else {
+                return 4 + length;
+            }
+        } else {
+            if (length > 255) {
+                return 5 + length;
+            } else {
+                return 2 + length;
+            }
+        }
+    }
+
+    @Override
+    public Symbol getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.SYMBOL;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() < size) {
+            return 0;
+        }
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                buffer.writeByte((byte) value.getLength());
+            } else {
+                buffer.writeInt(value.getLength());
+            }
+        } else if (value.getLength() <= 255) {
+            buffer.writeByte((byte) 0xa3);
+            buffer.writeByte((byte) value.getLength());
+        } else {
+            buffer.writeByte((byte) 0xb3);
+            buffer.writeByte((byte) value.getLength());
+        }
+        buffer.writeBytes(value.toString().getBytes(ASCII));
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/TimestampElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/TimestampElement.java
new file mode 100644
index 0000000..604ce4f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/TimestampElement.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.Date;
+
+import io.netty.buffer.ByteBuf;
+
+class TimestampElement extends AtomicElement<Date> {
+
+    private final Date value;
+
+    TimestampElement(Element<?> parent, Element<?> prev, Date d) {
+        super(parent, prev);
+        value = d;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 8 : 9;
+    }
+
+    @Override
+    public Date getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.TIMESTAMP;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (size > buffer.maxWritableBytes()) {
+            return 0;
+        }
+        if (size == 9) {
+            buffer.writeByte((byte) 0x83);
+        }
+        buffer.writeLong(value.getTime());
+
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/TypeDecoder.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/TypeDecoder.java
new file mode 100644
index 0000000..86eb1e6
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/TypeDecoder.java
@@ -0,0 +1,885 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.nio.charset.Charset;
+import java.util.Date;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal128;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal32;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Decimal64;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+class TypeDecoder {
+
+    private static final Charset ASCII = Charset.forName("US-ASCII");
+    private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+    private static final TypeConstructor[] constructors = new TypeConstructor[256];
+
+    static {
+        constructors[0x00] = new DescribedTypeConstructor();
+
+        constructors[0x40] = new NullConstructor();
+        constructors[0x41] = new TrueConstructor();
+        constructors[0x42] = new FalseConstructor();
+        constructors[0x43] = new UInt0Constructor();
+        constructors[0x44] = new ULong0Constructor();
+        constructors[0x45] = new EmptyListConstructor();
+
+        constructors[0x50] = new UByteConstructor();
+        constructors[0x51] = new ByteConstructor();
+        constructors[0x52] = new SmallUIntConstructor();
+        constructors[0x53] = new SmallULongConstructor();
+        constructors[0x54] = new SmallIntConstructor();
+        constructors[0x55] = new SmallLongConstructor();
+        constructors[0x56] = new BooleanConstructor();
+
+        constructors[0x60] = new UShortConstructor();
+        constructors[0x61] = new ShortConstructor();
+
+        constructors[0x70] = new UIntConstructor();
+        constructors[0x71] = new IntConstructor();
+        constructors[0x72] = new FloatConstructor();
+        constructors[0x73] = new CharConstructor();
+        constructors[0x74] = new Decimal32Constructor();
+
+        constructors[0x80] = new ULongConstructor();
+        constructors[0x81] = new LongConstructor();
+        constructors[0x82] = new DoubleConstructor();
+        constructors[0x83] = new TimestampConstructor();
+        constructors[0x84] = new Decimal64Constructor();
+
+        constructors[0x94] = new Decimal128Constructor();
+        constructors[0x98] = new UUIDConstructor();
+
+        constructors[0xa0] = new SmallBinaryConstructor();
+        constructors[0xa1] = new SmallStringConstructor();
+        constructors[0xa3] = new SmallSymbolConstructor();
+
+        constructors[0xb0] = new BinaryConstructor();
+        constructors[0xb1] = new StringConstructor();
+        constructors[0xb3] = new SymbolConstructor();
+
+        constructors[0xc0] = new SmallListConstructor();
+        constructors[0xc1] = new SmallMapConstructor();
+
+        constructors[0xd0] = new ListConstructor();
+        constructors[0xd1] = new MapConstructor();
+
+        constructors[0xe0] = new SmallArrayConstructor();
+        constructors[0xf0] = new ArrayConstructor();
+    }
+
+    private interface TypeConstructor {
+
+        Codec.DataType getType();
+
+        int size(ByteBuf buffer);
+
+        void parse(ByteBuf buffer, Codec data);
+
+    }
+
+    static int decode(ByteBuf buffer, Codec data) {
+        if (buffer.isReadable()) {
+            int position = buffer.readerIndex();
+            TypeConstructor c = readConstructor(buffer);
+            final int size = c.size(buffer);
+            if (buffer.readableBytes() >= size) {
+                c.parse(buffer, data);
+                return 1 + size;
+            } else {
+                buffer.readerIndex(position);
+                return -4;
+            }
+        }
+        return 0;
+    }
+
+    private static TypeConstructor readConstructor(ByteBuf buffer) {
+        int index = buffer.readByte() & 0xff;
+        TypeConstructor tc = constructors[index];
+        if (tc == null) {
+            throw new IllegalArgumentException("No constructor for type " + index);
+        }
+        return tc;
+    }
+
+    private static class NullConstructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.NULL;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 0;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putNull();
+        }
+    }
+
+    private static class TrueConstructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.BOOL;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 0;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putBoolean(true);
+        }
+    }
+
+    private static class FalseConstructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.BOOL;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 0;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putBoolean(false);
+        }
+    }
+
+    private static class UInt0Constructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.UINT;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 0;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedInteger(UnsignedInteger.ZERO);
+        }
+    }
+
+    private static class ULong0Constructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.ULONG;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 0;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedLong(UnsignedLong.ZERO);
+        }
+    }
+
+    private static class EmptyListConstructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.LIST;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 0;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putList();
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private static abstract class Fixed0SizeConstructor implements TypeConstructor {
+
+        @Override
+        public final int size(ByteBuf buffer) {
+            return 0;
+        }
+    }
+
+    private static abstract class Fixed1SizeConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 1;
+        }
+    }
+
+    private static abstract class Fixed2SizeConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 2;
+        }
+    }
+
+    private static abstract class Fixed4SizeConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 4;
+        }
+    }
+
+    private static abstract class Fixed8SizeConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 8;
+        }
+    }
+
+    private static abstract class Fixed16SizeConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            return 16;
+        }
+    }
+
+    private static class UByteConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.UBYTE;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedByte(UnsignedByte.valueOf(buffer.readByte()));
+        }
+    }
+
+    private static class ByteConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.BYTE;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putByte(buffer.readByte());
+        }
+    }
+
+    private static class SmallUIntConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.UINT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedInteger(UnsignedInteger.valueOf((buffer.readByte()) & 0xff));
+        }
+    }
+
+    private static class SmallIntConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.INT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putInt(buffer.readByte());
+        }
+    }
+
+    private static class SmallULongConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.ULONG;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedLong(UnsignedLong.valueOf((buffer.readByte()) & 0xff));
+        }
+    }
+
+    private static class SmallLongConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.LONG;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putLong(buffer.readByte());
+        }
+    }
+
+    private static class BooleanConstructor extends Fixed1SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.BOOL;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int i = buffer.readByte();
+            if (i != 0 && i != 1) {
+                throw new IllegalArgumentException("Illegal value " + i + " for boolean");
+            }
+            data.putBoolean(i == 1);
+        }
+    }
+
+    private static class UShortConstructor extends Fixed2SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.USHORT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedShort(UnsignedShort.valueOf(buffer.readShort()));
+        }
+    }
+
+    private static class ShortConstructor extends Fixed2SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.SHORT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putShort(buffer.readShort());
+        }
+    }
+
+    private static class UIntConstructor extends Fixed4SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.UINT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedInteger(UnsignedInteger.valueOf(buffer.readInt()));
+        }
+    }
+
+    private static class IntConstructor extends Fixed4SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.INT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putInt(buffer.readInt());
+        }
+    }
+
+    private static class FloatConstructor extends Fixed4SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.FLOAT;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putFloat(buffer.readFloat());
+        }
+    }
+
+    private static class CharConstructor extends Fixed4SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.CHAR;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putChar(buffer.readInt());
+        }
+    }
+
+    private static class Decimal32Constructor extends Fixed4SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.DECIMAL32;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putDecimal32(new Decimal32(buffer.readInt()));
+        }
+    }
+
+    private static class ULongConstructor extends Fixed8SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.ULONG;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUnsignedLong(UnsignedLong.valueOf(buffer.readLong()));
+        }
+    }
+
+    private static class LongConstructor extends Fixed8SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.LONG;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putLong(buffer.readLong());
+        }
+    }
+
+    private static class DoubleConstructor extends Fixed8SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.DOUBLE;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putDouble(buffer.readDouble());
+        }
+    }
+
+    private static class TimestampConstructor extends Fixed8SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.TIMESTAMP;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putTimestamp(new Date(buffer.readLong()));
+        }
+    }
+
+    private static class Decimal64Constructor extends Fixed8SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.DECIMAL64;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putDecimal64(new Decimal64(buffer.readLong()));
+        }
+    }
+
+    private static class Decimal128Constructor extends Fixed16SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.DECIMAL128;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putDecimal128(new Decimal128(buffer.readLong(), buffer.readLong()));
+        }
+    }
+
+    private static class UUIDConstructor extends Fixed16SizeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.UUID;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putUUID(new UUID(buffer.readLong(), buffer.readLong()));
+        }
+    }
+
+    private static abstract class SmallVariableConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            int position = buffer.readerIndex();
+            if (buffer.isReadable()) {
+                int size = buffer.readByte() & 0xff;
+                buffer.readerIndex(position);
+
+                return size + 1;
+            } else {
+                return 1;
+            }
+        }
+
+    }
+
+    private static abstract class VariableConstructor implements TypeConstructor {
+
+        @Override
+        public int size(ByteBuf buffer) {
+            int position = buffer.readerIndex();
+            if (buffer.readableBytes() >= 4) {
+                int size = buffer.readInt();
+                buffer.readerIndex(position);
+
+                return size + 4;
+            } else {
+                return 4;
+            }
+        }
+
+    }
+
+    private static class SmallBinaryConstructor extends SmallVariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.BINARY;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readByte() & 0xff;
+            byte[] bytes = new byte[size];
+            buffer.readBytes(bytes);
+            data.putBinary(bytes);
+        }
+    }
+
+    private static class SmallSymbolConstructor extends SmallVariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.SYMBOL;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readByte() & 0xff;
+            byte[] bytes = new byte[size];
+            buffer.readBytes(bytes);
+            data.putSymbol(Symbol.valueOf(new String(bytes, ASCII)));
+        }
+    }
+
+    private static class SmallStringConstructor extends SmallVariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.STRING;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readByte() & 0xff;
+            byte[] bytes = new byte[size];
+            buffer.readBytes(bytes);
+            data.putString(new String(bytes, UTF_8));
+        }
+    }
+
+    private static class BinaryConstructor extends VariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.BINARY;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readInt();
+            byte[] bytes = new byte[size];
+            buffer.readBytes(bytes);
+            data.putBinary(bytes);
+        }
+    }
+
+    private static class SymbolConstructor extends VariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.SYMBOL;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readInt();
+            byte[] bytes = new byte[size];
+            buffer.readBytes(bytes);
+            data.putSymbol(Symbol.valueOf(new String(bytes, ASCII)));
+        }
+    }
+
+    private static class StringConstructor extends VariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.STRING;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readInt();
+            byte[] bytes = new byte[size];
+            buffer.readBytes(bytes);
+            data.putString(new String(bytes, UTF_8));
+        }
+    }
+
+    private static class SmallListConstructor extends SmallVariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.LIST;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readByte() & 0xff;
+            ByteBuf buf = buffer.slice(buffer.readerIndex(), size);
+            buffer.skipBytes(size);
+            int count = buf.readByte() & 0xff;
+            data.putList();
+            parseChildren(data, buf, count);
+        }
+    }
+
+    private static class SmallMapConstructor extends SmallVariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.MAP;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readByte() & 0xff;
+            ByteBuf buf = buffer.slice(buffer.readerIndex(), size);
+            buffer.skipBytes(size);
+            int count = buf.readByte() & 0xff;
+            data.putMap();
+            parseChildren(data, buf, count);
+        }
+    }
+
+    private static class ListConstructor extends VariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.LIST;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readInt();
+            ByteBuf buf = buffer.slice(buffer.readerIndex(), size);
+            buffer.skipBytes(size);
+            int count = buf.readInt();
+            data.putList();
+            parseChildren(data, buf, count);
+        }
+    }
+
+    private static class MapConstructor extends VariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.MAP;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            int size = buffer.readInt();
+            ByteBuf buf = buffer.slice(buffer.readerIndex(), size);
+            buffer.skipBytes(size);
+            int count = buf.readInt();
+            data.putMap();
+            parseChildren(data, buf, count);
+        }
+    }
+
+    private static void parseChildren(Codec data, ByteBuf buf, int count) {
+        data.enter();
+        for (int i = 0; i < count; i++) {
+            TypeConstructor c = readConstructor(buf);
+            final int size = c.size(buf);
+            final int getReadableBytes = buf.readableBytes();
+            if (size <= getReadableBytes) {
+                c.parse(buf, data);
+            } else {
+                throw new IllegalArgumentException("Malformed data");
+            }
+
+        }
+        data.exit();
+    }
+
+    private static class DescribedTypeConstructor implements TypeConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.DESCRIBED;
+        }
+
+        @Override
+        public int size(ByteBuf buffer) {
+            ByteBuf buf = buffer.slice();
+            if (buf.isReadable()) {
+                TypeConstructor c = readConstructor(buf);
+                int size = c.size(buf);
+                if (buf.readableBytes() > size) {
+                    buf.readerIndex(size + 1);
+                    c = readConstructor(buf);
+                    return size + 2 + c.size(buf);
+                } else {
+                    return size + 2;
+                }
+            } else {
+                return 1;
+            }
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+            data.putDescribed();
+            data.enter();
+            TypeConstructor c = readConstructor(buffer);
+            c.parse(buffer, data);
+            c = readConstructor(buffer);
+            c.parse(buffer, data);
+            data.exit();
+        }
+    }
+
+    private static class SmallArrayConstructor extends SmallVariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.ARRAY;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+
+            int size = buffer.readByte() & 0xff;
+            ByteBuf buf = buffer.slice(buffer.readerIndex(), size);
+            buffer.skipBytes(size);
+            int count = buf.readByte() & 0xff;
+            parseArray(data, buf, count);
+        }
+    }
+
+    private static class ArrayConstructor extends VariableConstructor {
+
+        @Override
+        public Codec.DataType getType() {
+            return Codec.DataType.ARRAY;
+        }
+
+        @Override
+        public void parse(ByteBuf buffer, Codec data) {
+
+            int size = buffer.readInt();
+            ByteBuf buf = buffer.slice(buffer.readerIndex(), size);
+            buffer.skipBytes(size);
+            int count = buf.readInt();
+            parseArray(data, buf, count);
+        }
+    }
+
+    private static void parseArray(Codec data, ByteBuf buffer, int count) {
+        byte type = buffer.readByte();
+        boolean isDescribed = type == (byte) 0x00;
+        int descriptorPosition = buffer.readerIndex();
+        if (isDescribed) {
+            TypeConstructor descriptorTc = readConstructor(buffer);
+            buffer.skipBytes(descriptorTc.size(buffer));
+            type = buffer.readByte();
+            if (type == (byte) 0x00) {
+                throw new IllegalArgumentException("Malformed array data");
+            }
+
+        }
+        TypeConstructor tc = constructors[type & 0xff];
+
+        data.putArray(isDescribed, tc.getType());
+        data.enter();
+        if (isDescribed) {
+            int position = buffer.readerIndex();
+            buffer.readerIndex(descriptorPosition);
+            TypeConstructor descriptorTc = readConstructor(buffer);
+            descriptorTc.parse(buffer, data);
+            buffer.readerIndex(position);
+        }
+        for (int i = 0; i < count; i++) {
+            tc.parse(buffer, data);
+        }
+
+        data.exit();
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UUIDElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UUIDElement.java
new file mode 100644
index 0000000..62a0cbc
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UUIDElement.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import java.util.UUID;
+
+import io.netty.buffer.ByteBuf;
+
+class UUIDElement extends AtomicElement<UUID> {
+
+    private final UUID value;
+
+    UUIDElement(Element<?> parent, Element<?> prev, UUID u) {
+        super(parent, prev);
+        value = u;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 16 : 17;
+    }
+
+    @Override
+    public UUID getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.UUID;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (buffer.maxWritableBytes() >= size) {
+            if (size == 17) {
+                buffer.writeByte((byte) 0x98);
+            }
+            buffer.writeLong(value.getMostSignificantBits());
+            buffer.writeLong(value.getLeastSignificantBits());
+            return size;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedByteElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedByteElement.java
new file mode 100644
index 0000000..6dff1fb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedByteElement.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+
+import io.netty.buffer.ByteBuf;
+
+class UnsignedByteElement extends AtomicElement<UnsignedByte> {
+
+    private final UnsignedByte value;
+
+    UnsignedByteElement(Element<?> parent, Element<?> prev, UnsignedByte ub) {
+        super(parent, prev);
+        value = ub;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 1 : 2;
+    }
+
+    @Override
+    public UnsignedByte getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.UBYTE;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        if (isElementOfArray()) {
+            if (buffer.isWritable()) {
+                buffer.writeByte(value.byteValue());
+                return 1;
+            }
+        } else {
+            if (buffer.maxWritableBytes() >= 2) {
+                buffer.writeByte((byte) 0x50);
+                buffer.writeByte(value.byteValue());
+                return 2;
+            }
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedIntegerElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedIntegerElement.java
new file mode 100644
index 0000000..0ee7137
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedIntegerElement.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+
+import io.netty.buffer.ByteBuf;
+
+class UnsignedIntegerElement extends AtomicElement<UnsignedInteger> {
+
+    private final UnsignedInteger value;
+
+    UnsignedIntegerElement(Element<?> parent, Element<?> prev, UnsignedInteger i) {
+        super(parent, prev);
+        value = i;
+    }
+
+    @Override
+    public int size() {
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+            if (parent.constructorType() == ArrayElement.TINY) {
+                if (value.intValue() == 0) {
+                    return 0;
+                } else {
+                    parent.setConstructorType(ArrayElement.SMALL);
+                }
+            }
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (0 <= value.intValue() && value.intValue() <= 255) {
+                    return 1;
+                } else {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                }
+            }
+
+            return 4;
+
+        } else {
+            return 0 == value.intValue() ? 1 : (1 <= value.intValue() && value.intValue() <= 255) ? 2 : 5;
+        }
+
+    }
+
+    @Override
+    public UnsignedInteger getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.UINT;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (size > buffer.maxWritableBytes()) {
+            return 0;
+        }
+        switch (size) {
+            case 1:
+                if (isElementOfArray()) {
+                    buffer.writeByte((byte) value.intValue());
+                } else {
+                    buffer.writeByte((byte) 0x43);
+                }
+                break;
+            case 2:
+                buffer.writeByte((byte) 0x52);
+                buffer.writeByte((byte) value.intValue());
+                break;
+            case 5:
+                buffer.writeByte((byte) 0x70);
+            case 4:
+                buffer.writeInt(value.intValue());
+
+        }
+
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedLongElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedLongElement.java
new file mode 100644
index 0000000..cf04e5d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedLongElement.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+class UnsignedLongElement extends AtomicElement<UnsignedLong> {
+
+    private final UnsignedLong value;
+
+    UnsignedLongElement(Element<?> parent, Element<?> prev, UnsignedLong ul) {
+        super(parent, prev);
+        value = ul;
+    }
+
+    @Override
+    public int size() {
+        if (isElementOfArray()) {
+            final ArrayElement parent = (ArrayElement) parent();
+            if (parent.constructorType() == ArrayElement.TINY) {
+                if (value.longValue() == 0l) {
+                    return 0;
+                } else {
+                    parent.setConstructorType(ArrayElement.SMALL);
+                }
+            }
+
+            if (parent.constructorType() == ArrayElement.SMALL) {
+                if (0l <= value.longValue() && value.longValue() <= 255l) {
+                    return 1;
+                } else {
+                    parent.setConstructorType(ArrayElement.LARGE);
+                }
+            }
+
+            return 8;
+
+        } else {
+            return 0l == value.longValue() ? 1 : (1l <= value.longValue() && value.longValue() <= 255l) ? 2 : 9;
+        }
+
+    }
+
+    @Override
+    public UnsignedLong getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.ULONG;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        int size = size();
+        if (size > buffer.maxWritableBytes()) {
+            return 0;
+        }
+        switch (size) {
+            case 1:
+                if (isElementOfArray()) {
+                    buffer.writeByte((byte) value.longValue());
+                } else {
+                    buffer.writeByte((byte) 0x44);
+                }
+                break;
+            case 2:
+                buffer.writeByte((byte) 0x53);
+                buffer.writeByte((byte) value.longValue());
+                break;
+            case 9:
+                buffer.writeByte((byte) 0x80);
+            case 8:
+                buffer.writeLong(value.longValue());
+
+        }
+
+        return size;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedShortElement.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedShortElement.java
new file mode 100644
index 0000000..9967cfd
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/UnsignedShortElement.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+class UnsignedShortElement extends AtomicElement<UnsignedShort> {
+
+    private final UnsignedShort value;
+
+    UnsignedShortElement(Element<?> parent, Element<?> prev, UnsignedShort ub) {
+        super(parent, prev);
+        value = ub;
+    }
+
+    @Override
+    public int size() {
+        return isElementOfArray() ? 2 : 3;
+    }
+
+    @Override
+    public UnsignedShort getValue() {
+        return value;
+    }
+
+    @Override
+    public Codec.DataType getDataType() {
+        return Codec.DataType.USHORT;
+    }
+
+    @Override
+    public int encode(ByteBuf buffer) {
+        if (isElementOfArray()) {
+            if (buffer.maxWritableBytes() >= 2) {
+                buffer.writeShort(value.shortValue());
+                return 2;
+            }
+        } else {
+            if (buffer.maxWritableBytes() >= 3) {
+                buffer.writeByte((byte) 0x60);
+                buffer.writeShort(value.shortValue());
+                return 3;
+            }
+        }
+        return 0;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Accepted.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Accepted.java
new file mode 100644
index 0000000..2681e26
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Accepted.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+
+/**
+ * Basic Described type that should contain a List as the value.
+ */
+public class Accepted extends ListDescribedType implements DeliveryState, Outcome {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:accepted:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000024L);
+
+    private static final Accepted INSTANCE = new Accepted();
+
+    public Accepted() {
+        super(0);
+    }
+
+    public static Accepted getInstance() {
+        return INSTANCE;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Accepted(Object described) {
+        super(0, (List<Object>) described);
+    }
+
+    public Accepted(List<Object> described) {
+        super(0, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Accepted;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/AmqpSequence.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/AmqpSequence.java
new file mode 100644
index 0000000..6375a90
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/AmqpSequence.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class AmqpSequence implements DescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000076L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:amqp-sequence:list");
+
+    private List<?> described;
+
+    public AmqpSequence(List<?> described) {
+        this.described = described;
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Object getDescribed() {
+        return described;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/AmqpValue.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/AmqpValue.java
new file mode 100644
index 0000000..3ac8d16
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/AmqpValue.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class AmqpValue implements DescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000077L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:amqp-value:*");
+
+    private Object described;
+
+    public AmqpValue(Object described) {
+        this.described = described;
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Object getDescribed() {
+        return described;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/ApplicationProperties.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/ApplicationProperties.java
new file mode 100644
index 0000000..7ad3bae
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/ApplicationProperties.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.MapDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class ApplicationProperties extends MapDescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000074L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:application-properties:map");
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public void setApplicationProperty(String name, Object value) {
+        if (name == null) {
+            throw new RuntimeException("ApplicationProperties maps must use non-null String keys");
+        }
+
+        getDescribed().put(name, value);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Data.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Data.java
new file mode 100644
index 0000000..e0860ce
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Data.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Data implements DescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000075L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:data:binary");
+
+    private Binary described;
+
+    public Data(Binary described) {
+        if (described == null) {
+            throw new IllegalArgumentException("provided Binary must not be null");
+        }
+
+        this.described = described;
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Binary getDescribed() {
+        return described;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnClose.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnClose.java
new file mode 100644
index 0000000..21ea1f2
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnClose.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class DeleteOnClose extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-close:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002bL);
+
+    public DeleteOnClose() {
+        super(0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public DeleteOnClose(Object described) {
+        super(0, (List<Object>) described);
+    }
+
+    public DeleteOnClose(List<Object> described) {
+        super(0, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoLinks.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoLinks.java
new file mode 100644
index 0000000..8e06df8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoLinks.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class DeleteOnNoLinks extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-no-links:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002cL);
+
+    public DeleteOnNoLinks() {
+        super(0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public DeleteOnNoLinks(Object described) {
+        super(0, (List<Object>) described);
+    }
+
+    public DeleteOnNoLinks(List<Object> described) {
+        super(0, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoLinksOrMessages.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoLinksOrMessages.java
new file mode 100644
index 0000000..f72e8fc
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoLinksOrMessages.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class DeleteOnNoLinksOrMessages extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-no-links-or-messages:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002eL);
+
+    public DeleteOnNoLinksOrMessages() {
+        super(0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public DeleteOnNoLinksOrMessages(Object described) {
+        super(0, (List<Object>) described);
+    }
+
+    public DeleteOnNoLinksOrMessages(List<Object> described) {
+        super(0, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoMessages.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoMessages.java
new file mode 100644
index 0000000..d62e17f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeleteOnNoMessages.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class DeleteOnNoMessages extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-no-messages:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002dL);
+
+    public DeleteOnNoMessages() {
+        super(0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public DeleteOnNoMessages(Object described) {
+        super(0, (List<Object>) described);
+    }
+
+    public DeleteOnNoMessages(List<Object> described) {
+        super(0, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeliveryAnnotations.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeliveryAnnotations.java
new file mode 100644
index 0000000..4373743
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/DeliveryAnnotations.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.MapDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class DeliveryAnnotations extends MapDescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000071L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delivery-annotations:map");
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public void setSymbolKeyedAnnotation(String name, Object value) {
+        getDescribed().put(Symbol.valueOf(name), value);
+    }
+
+    public void setSymbolKeyedAnnotation(Symbol name, Object value) {
+        getDescribed().put(name, value);
+    }
+
+    public void setUnsignedLongKeyedAnnotation(UnsignedLong name, Object value) {
+        throw new UnsupportedOperationException("UnsignedLong keys are currently reserved");
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Footer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Footer.java
new file mode 100644
index 0000000..f6db954
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Footer.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.MapDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Footer extends MapDescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000078L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:footer:map");
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public void setFooterProperty(Object key, Object value) {
+        if (key == null) {
+            throw new RuntimeException("Footer maps must use non-null keys");
+        }
+
+        getDescribed().put(key, value);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Header.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Header.java
new file mode 100644
index 0000000..a7f28dc
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Header.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Header extends ListDescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000070L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:header:list");
+
+    /**
+     * Enumeration which maps to fields in the Header Performative
+     */
+    public enum Field {
+        DURABLE,
+        PRIORITY,
+        TTL,
+        FIRST_ACQUIRER,
+        DELIVERY_COUNT,
+    }
+
+    public Header() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Header(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Header(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Header setDurable(Boolean o) {
+        getList().set(Field.DURABLE.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getDurable() {
+        return (Boolean) getList().get(Field.DURABLE.ordinal());
+    }
+
+    public Header setPriority(UnsignedByte o) {
+        getList().set(Field.PRIORITY.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedByte getPriority() {
+        return (UnsignedByte) getList().get(Field.PRIORITY.ordinal());
+    }
+
+    public Header setTtl(UnsignedInteger o) {
+        getList().set(Field.TTL.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getTtl() {
+        return (UnsignedInteger) getList().get(Field.TTL.ordinal());
+    }
+
+    public Header setFirstAcquirer(Boolean o) {
+        getList().set(Field.FIRST_ACQUIRER.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getFirstAcquirer() {
+        return (Boolean) getList().get(Field.FIRST_ACQUIRER.ordinal());
+    }
+
+    public Header setDeliveryCount(UnsignedInteger o) {
+        getList().set(Field.DELIVERY_COUNT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getDeliveryCount() {
+        return (UnsignedInteger) getList().get(Field.DELIVERY_COUNT.ordinal());
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/MessageAnnotations.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/MessageAnnotations.java
new file mode 100644
index 0000000..73f656d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/MessageAnnotations.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.MapDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class MessageAnnotations extends MapDescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000072L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:message-annotations:map");
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public void setSymbolKeyedAnnotation(String name, Object value) {
+        getDescribed().put(Symbol.valueOf(name), value);
+    }
+
+    public void setSymbolKeyedAnnotation(Symbol name, Object value) {
+        getDescribed().put(name, value);
+    }
+
+    public void setUnsignedLongKeyedAnnotation(UnsignedLong name, Object value) {
+        throw new UnsupportedOperationException("UnsignedLong keys are currently reserved");
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Modified.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Modified.java
new file mode 100644
index 0000000..69cb74e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Modified.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+
+public class Modified extends ListDescribedType implements DeliveryState, Outcome {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:modified:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000027L);
+
+    /**
+     * Enumeration which maps to fields in the Modified Performative
+     */
+    public enum Field {
+        DELIVERY_FAILED,
+        UNDELIVERABLE_HERE,
+        MESSAGE_ANNOTATIONS
+    }
+
+    public Modified() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Modified(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Modified(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Modified setDeliveryFailed(Boolean o) {
+        getList().set(Field.DELIVERY_FAILED.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getDeliveryFailed() {
+        return (Boolean) getList().get(Field.DELIVERY_FAILED.ordinal());
+    }
+
+    public Modified setUndeliverableHere(Boolean o) {
+        getList().set(Field.UNDELIVERABLE_HERE.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getUndeliverableHere() {
+        return (Boolean) getList().get(Field.UNDELIVERABLE_HERE.ordinal());
+    }
+
+    public Modified setMessageAnnotations(Map<Symbol, Object> o) {
+        getList().set(Field.MESSAGE_ANNOTATIONS.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getMessageAnnotations() {
+        return (Map<Symbol, Object>) getList().get(Field.MESSAGE_ANNOTATIONS.ordinal());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Modified;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Outcome.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Outcome.java
new file mode 100644
index 0000000..573629d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Outcome.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+public interface Outcome {
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Properties.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Properties.java
new file mode 100644
index 0000000..16e37b9
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Properties.java
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Properties extends ListDescribedType {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000073L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:properties:list");
+
+    /**
+     * Enumeration which maps to fields in the Properties Performative
+     */
+    public enum Field {
+        MESSAGE_ID,
+        USER_ID,
+        TO,
+        SUBJECT,
+        REPLY_TO,
+        CORRELATION_ID,
+        CONTENT_TYPE,
+        CONTENT_ENCODING,
+        ABSOLUTE_EXPIRY_TIME,
+        CREATION_TIME,
+        GROUP_ID,
+        GROUP_SEQUENCE,
+        REPLY_TO_GROUP_ID,
+    }
+
+    public Properties() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Properties(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Properties(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Properties setMessageId(Object o) {
+        getList().set(Field.MESSAGE_ID.ordinal(), o);
+        return this;
+    }
+
+    public Object getMessageId() {
+        return getList().get(Field.MESSAGE_ID.ordinal());
+    }
+
+    public Properties setUserId(Binary o) {
+        getList().set(Field.USER_ID.ordinal(), o);
+        return this;
+    }
+
+    public Binary getUserId() {
+        return (Binary) getList().get(Field.USER_ID.ordinal());
+    }
+
+    public Properties setTo(String o) {
+        getList().set(Field.TO.ordinal(), o);
+        return this;
+    }
+
+    public String getTo() {
+        return (String) getList().get(Field.TO.ordinal());
+    }
+
+    public Properties setSubject(String o) {
+        getList().set(Field.SUBJECT.ordinal(), o);
+        return this;
+    }
+
+    public String getSubject() {
+        return (String) getList().get(Field.SUBJECT.ordinal());
+    }
+
+    public Properties setReplyTo(String o) {
+        getList().set(Field.REPLY_TO.ordinal(), o);
+        return this;
+    }
+
+    public String getReplyTo() {
+        return (String) getList().get(Field.REPLY_TO.ordinal());
+    }
+
+    public Properties setCorrelationId(Object o) {
+        getList().set(Field.CORRELATION_ID.ordinal(), o);
+        return this;
+    }
+
+    public Object getCorrelationId() {
+        return getList().get(Field.CORRELATION_ID.ordinal());
+    }
+
+    public Properties setContentType(Symbol o) {
+        getList().set(Field.CONTENT_TYPE.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getContentType() {
+        return (Symbol) getList().get(Field.CONTENT_TYPE.ordinal());
+    }
+
+    public Properties setContentEncoding(Symbol o) {
+        getList().set(Field.CONTENT_ENCODING.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getContentEncoding() {
+        return (Symbol) getList().get(Field.CONTENT_ENCODING.ordinal());
+    }
+
+    public Properties setAbsoluteExpiryTime(Date o) {
+        getList().set(Field.ABSOLUTE_EXPIRY_TIME.ordinal(), o);
+        return this;
+    }
+
+    public Date getAbsoluteExpiryTime() {
+        return (Date) getList().get(Field.ABSOLUTE_EXPIRY_TIME.ordinal());
+    }
+
+    public Properties setCreationTime(Date o) {
+        getList().set(Field.CREATION_TIME.ordinal(), o);
+        return this;
+    }
+
+    public Date getCreationTime() {
+        return (Date) getList().get(Field.CREATION_TIME.ordinal());
+    }
+
+    public Properties setGroupId(String o) {
+        getList().set(Field.GROUP_ID.ordinal(), o);
+        return this;
+    }
+
+    public String getGroupId() {
+        return (String) getList().get(Field.GROUP_ID.ordinal());
+    }
+
+    public Properties setGroupSequence(UnsignedInteger o) {
+        getList().set(Field.GROUP_SEQUENCE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getGroupSequence() {
+        return (UnsignedInteger) getList().get(Field.GROUP_SEQUENCE.ordinal());
+    }
+
+    public Properties setReplyToGroupId(String o) {
+        getList().set(Field.REPLY_TO_GROUP_ID.ordinal(), o);
+        return this;
+    }
+
+    public String getReplyToGroupId() {
+        return (String) getList().get(Field.REPLY_TO_GROUP_ID.ordinal());
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Received.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Received.java
new file mode 100644
index 0000000..73382eb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Received.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+
+public class Received extends ListDescribedType implements DeliveryState, Outcome {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:received:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000023L);
+
+    /**
+     * Enumeration which maps to fields in the Received Performative
+     */
+    public enum Field {
+        SECTION_NUMBER,
+        SECTION_OFFSET,
+    }
+
+    public Received() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Received(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Received(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Received setSectionNumber(Object o) {
+        getList().set(Field.SECTION_NUMBER.ordinal(), o);
+        return this;
+    }
+
+    public Object getSectionNumber() {
+        return getList().get(Field.SECTION_NUMBER.ordinal());
+    }
+
+    public Received setSectionOffset(Object o) {
+        getList().set(Field.SECTION_OFFSET.ordinal(), o);
+        return this;
+    }
+
+    public Object getSectionOffset() {
+        return getList().get(Field.SECTION_OFFSET.ordinal());
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Received;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Rejected.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Rejected.java
new file mode 100644
index 0000000..fc2b1ab
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Rejected.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+
+public class Rejected extends ListDescribedType implements DeliveryState, Outcome {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:rejected:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000025L);
+
+    /**
+     * Enumeration which maps to fields in the Rejected Performative
+     */
+    public enum Field {
+        ERROR,
+    }
+
+    public Rejected() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Rejected(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Rejected(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public UnsignedLong getDescriptor() {
+        return DESCRIPTOR_CODE;
+    }
+
+    public Rejected setError(ErrorCondition o) {
+        getList().set(Field.ERROR.ordinal(), o);
+        return this;
+    }
+
+    public ErrorCondition getError() {
+        return (ErrorCondition) getList().get(Field.ERROR.ordinal());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Rejected;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Released.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Released.java
new file mode 100644
index 0000000..296d6c5
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Released.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+
+public class Released extends ListDescribedType implements DeliveryState, Outcome {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:released:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000026L);
+
+    private static final Released INSTANCE = new Released();
+
+    public Released() {
+        super(0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Released(Object described) {
+        super(0, (List<Object>) described);
+    }
+
+    public Released(List<Object> described) {
+        super(0, described);
+    }
+
+    public static Released getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Released;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Source.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Source.java
new file mode 100644
index 0000000..3b2f224
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Source.java
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+
+public class Source extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:source:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000028L);
+
+    /**
+     * Enumeration which maps to fields in the Source Performative
+     */
+    public enum Field {
+        ADDRESS,
+        DURABLE,
+        EXPIRY_POLICY,
+        TIMEOUT,
+        DYNAMIC,
+        DYNAMIC_NODE_PROPERTIES,
+        DISTRIBUTION_MODE,
+        FILTER,
+        DEFAULT_OUTCOME,
+        OUTCOMES,
+        CAPABILITIES,
+    }
+
+    public Source() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Source(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Source(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    public Source(Source value) {
+        super(Field.values().length, value);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Source setAddress(String o) {
+        getList().set(Field.ADDRESS.ordinal(), o);
+        return this;
+    }
+
+    public String getAddress() {
+        return (String) getList().get(Field.ADDRESS.ordinal());
+    }
+
+    public Source setDurable(UnsignedInteger o) {
+        getList().set(Field.DURABLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getDurable() {
+        return (UnsignedInteger) getList().get(Field.DURABLE.ordinal());
+    }
+
+    public Source setExpiryPolicy(Symbol o) {
+        getList().set(Field.EXPIRY_POLICY.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getExpiryPolicy() {
+        return (Symbol) getList().get(Field.EXPIRY_POLICY.ordinal());
+    }
+
+    public Source setTimeout(UnsignedInteger o) {
+        getList().set(Field.TIMEOUT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getTimeout() {
+        return (UnsignedInteger) getList().get(Field.TIMEOUT.ordinal());
+    }
+
+    public Source setDynamic(Boolean o) {
+        getList().set(Field.DYNAMIC.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getDynamic() {
+        return (Boolean) getList().get(Field.DYNAMIC.ordinal());
+    }
+
+    public Source setDynamicNodeProperties(Map<Symbol, Object> o) {
+        getList().set(Field.DYNAMIC_NODE_PROPERTIES.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getDynamicNodeProperties() {
+        return (Map<Symbol, Object>) getList().get(Field.DYNAMIC_NODE_PROPERTIES.ordinal());
+    }
+
+    public Source setDistributionMode(Symbol o) {
+        getList().set(Field.DISTRIBUTION_MODE.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getDistributionMode() {
+        return (Symbol) getList().get(Field.DISTRIBUTION_MODE.ordinal());
+    }
+
+    public Source setFilter(Map<Symbol, Object> o) {
+        getList().set(Field.FILTER.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getFilter() {
+        return (Map<Symbol, Object>) getList().get(Field.FILTER.ordinal());
+    }
+
+    public Source setDefaultOutcome(DescribedType o) {
+        getList().set(Field.DEFAULT_OUTCOME.ordinal(), o);
+        return this;
+    }
+
+    public DescribedType getDefaultOutcome() {
+        return (DescribedType) getList().get(Field.DEFAULT_OUTCOME.ordinal());
+    }
+
+    public Source setOutcomes(Symbol... o) {
+        getList().set(Field.OUTCOMES.ordinal(), o);
+        return this;
+    }
+
+    public Source setOutcomes(String... o) {
+        getList().set(Field.OUTCOMES.ordinal(), TypeMapper.toSymbolArray(o));
+        return this;
+    }
+
+    public Symbol[] getOutcomes() {
+        return (Symbol[]) getList().get(Field.OUTCOMES.ordinal());
+    }
+
+    public Source setCapabilities(Symbol... o) {
+        getList().set(Field.CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Source setCapabilities(String... o) {
+        getList().set(Field.CAPABILITIES.ordinal(), TypeMapper.toSymbolArray(o));
+        return this;
+    }
+
+    public Symbol[] getCapabilities() {
+        return (Symbol[]) getList().get(Field.CAPABILITIES.ordinal());
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Target.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Target.java
new file mode 100644
index 0000000..6475a59
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/Target.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Target extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:target:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000029L);
+
+    /**
+     * Enumeration which maps to fields in the Target Performative
+     */
+    public enum Field {
+        ADDRESS,
+        DURABLE,
+        EXPIRY_POLICY,
+        TIMEOUT,
+        DYNAMIC,
+        DYNAMIC_NODE_PROPERTIES,
+        CAPABILITIES,
+    }
+
+    public Target() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Target(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Target(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    public Target(Target value) {
+        super(Field.values().length, value);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Target setAddress(String o) {
+        getList().set(Field.ADDRESS.ordinal(), o);
+        return this;
+    }
+
+    public String getAddress() {
+        return (String) getList().get(Field.ADDRESS.ordinal());
+    }
+
+    public Target setDurable(UnsignedInteger o) {
+        getList().set(Field.DURABLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getDurable() {
+        return (UnsignedInteger) getList().get(Field.DURABLE.ordinal());
+    }
+
+    public Target setExpiryPolicy(Symbol o) {
+        getList().set(Field.EXPIRY_POLICY.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getExpiryPolicy() {
+        return (Symbol) getList().get(Field.EXPIRY_POLICY.ordinal());
+    }
+
+    public Target setTimeout(UnsignedInteger o) {
+        getList().set(Field.TIMEOUT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getTimeout() {
+        return (UnsignedInteger) getList().get(Field.TIMEOUT.ordinal());
+    }
+
+    public Target setDynamic(Boolean o) {
+        getList().set(Field.DYNAMIC.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getDynamic() {
+        return (Boolean) getList().get(Field.DYNAMIC.ordinal());
+    }
+
+    public Target setDynamicNodeProperties(Map<Symbol, Object> o) {
+        getList().set(Field.DYNAMIC_NODE_PROPERTIES.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getDynamicNodeProperties() {
+        return (Map<Symbol, Object>) getList().get(Field.DYNAMIC_NODE_PROPERTIES.ordinal());
+    }
+
+    public Target setCapabilities(Symbol[] o) {
+        getList().set(Field.CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getCapabilities() {
+        return (Symbol[]) getList().get(Field.CAPABILITIES.ordinal());
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/TerminusDurability.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/TerminusDurability.java
new file mode 100644
index 0000000..77d355b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/TerminusDurability.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+
+public enum TerminusDurability {
+
+    NONE, CONFIGURATION, UNSETTLED_STATE;
+
+    public UnsignedInteger getValue() {
+        return UnsignedInteger.valueOf(ordinal());
+    }
+
+    public static TerminusDurability valueOf(UnsignedInteger value) {
+        return TerminusDurability.valueOf(value.intValue());
+    }
+
+    public static TerminusDurability valueOf(long value) {
+        if (value == 0) {
+            return NONE;
+        } else if (value == 1) {
+            return CONFIGURATION;
+        } else if (value == 2) {
+            return UNSETTLED_STATE;
+        }
+
+        throw new IllegalArgumentException("Unknown TerminusDurablity: " + value);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/TerminusExpiryPolicy.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/TerminusExpiryPolicy.java
new file mode 100644
index 0000000..f4b511a
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/messaging/TerminusExpiryPolicy.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.messaging;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+
+public enum TerminusExpiryPolicy {
+
+    LINK_DETACH("link-detach"),
+    SESSION_END("session-end"),
+    CONNECTION_CLOSE("connection-close"),
+    NEVER("never");
+
+    private Symbol policy;
+    private static final Map<Symbol, TerminusExpiryPolicy> map = new HashMap<>();
+
+    TerminusExpiryPolicy(String policy) {
+        this.policy = Symbol.valueOf(policy);
+    }
+
+    public Symbol getPolicy() {
+        return policy;
+    }
+
+    static {
+        map.put(LINK_DETACH.getPolicy(), LINK_DETACH);
+        map.put(SESSION_END.getPolicy(), SESSION_END);
+        map.put(CONNECTION_CLOSE.getPolicy(), CONNECTION_CLOSE);
+        map.put(NEVER.getPolicy(), NEVER);
+    }
+
+    public static TerminusExpiryPolicy valueOf(Symbol policy) {
+        TerminusExpiryPolicy expiryPolicy = map.get(policy);
+        if (expiryPolicy == null) {
+            throw new IllegalArgumentException("Unknown TerminusExpiryPolicy: " + policy);
+        }
+        return expiryPolicy;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Binary.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Binary.java
new file mode 100644
index 0000000..e1c19eb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Binary.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public final class Binary {
+
+    private final byte[] buffer;
+    private int hashCode;
+
+    public Binary() {
+        this.buffer = null;
+    }
+
+    public Binary(final byte[] data) {
+        this(data, 0, data.length);
+    }
+
+    public Binary(final byte[] data, final int offset, final int length) {
+        this.buffer = Arrays.copyOfRange(data, offset, offset + length);
+    }
+
+    public Binary copy() {
+        if (buffer == null) {
+            return new Binary();
+        } else {
+            return new Binary(Arrays.copyOf(buffer, buffer.length));
+        }
+    }
+
+    public byte[] arrayCopy() {
+        byte[] dataCopy = null;
+
+        if (buffer != null) {
+            dataCopy = Arrays.copyOf(buffer, buffer.length);
+        }
+
+        return dataCopy;
+    }
+
+    public ByteBuffer asByteBuffer() {
+        return buffer != null ? ByteBuffer.wrap(buffer) : null;
+    }
+
+    @Override
+    public final int hashCode() {
+        int hc = hashCode;
+        if (hc == 0 && buffer != null) {
+            hashCode = buffer.hashCode();
+        }
+        return hc;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Binary other = (Binary) o;
+        if (getLength() != other.getLength()) {
+            return false;
+        }
+
+        if (buffer == null) {
+            return other.buffer == null;
+        }
+
+        return Arrays.equals(buffer, other.buffer);
+    }
+
+    public boolean hasArray() {
+        return buffer != null;
+    }
+
+    public byte[] getArray() {
+        return buffer;
+    }
+
+    public int getLength() {
+        return buffer != null ? buffer.length : 0;
+    }
+
+    @Override
+    public String toString() {
+        if (buffer == null) {
+            return "";
+        }
+
+        StringBuilder str = new StringBuilder();
+
+        for (int i = 0; i < getLength(); i++) {
+            byte c = buffer[i];
+
+            if (c > 31 && c < 127 && c != '\\') {
+                str.append((char) c);
+            } else {
+                str.append(String.format("\\x%02x", c));
+            }
+        }
+
+        return str.toString();
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal128.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal128.java
new file mode 100644
index 0000000..e621324
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal128.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import java.lang.annotation.Native;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+
+public final class Decimal128 extends Number {
+
+    private static final long serialVersionUID = -4863018398624288737L;
+
+    /**
+     * The number of bits used to represent an {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    @Native public static final int SIZE = 128;
+
+    /**
+     * The number of bytes used to represent a {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    public static final int BYTES = SIZE / Byte.SIZE;
+
+    private final BigDecimal underlying;
+    private final long msb;
+    private final long lsb;
+
+    public Decimal128(BigDecimal underlying) {
+        this.underlying = underlying;
+
+        this.msb = calculateMostSignificantBits(underlying);
+        this.lsb = calculateLeastSignificantBits(underlying);
+    }
+
+    public Decimal128(final long msb, final long lsb) {
+        this.msb = msb;
+        this.lsb = lsb;
+
+        this.underlying = calculateBigDecimal(msb, lsb);
+    }
+
+    public Decimal128(byte[] data) {
+        this(ByteBuffer.wrap(data));
+    }
+
+    private Decimal128(final ByteBuffer buffer) {
+        this(buffer.getLong(), buffer.getLong());
+    }
+
+    private static long calculateMostSignificantBits(final BigDecimal underlying) {
+        return 0; // TODO.
+    }
+
+    private static long calculateLeastSignificantBits(final BigDecimal underlying) {
+        return 0; // TODO.
+    }
+
+    private static BigDecimal calculateBigDecimal(final long msb, final long lsb) {
+        return BigDecimal.ZERO; // TODO.
+    }
+
+    @Override
+    public int intValue() {
+        return underlying.intValue();
+    }
+
+    @Override
+    public long longValue() {
+        return underlying.longValue();
+    }
+
+    @Override
+    public float floatValue() {
+        return underlying.floatValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return underlying.doubleValue();
+    }
+
+    public long getMostSignificantBits() {
+        return msb;
+    }
+
+    public long getLeastSignificantBits() {
+        return lsb;
+    }
+
+    public byte[] asBytes() {
+        byte[] bytes = new byte[16];
+        ByteBuffer buf = ByteBuffer.wrap(bytes);
+        buf.putLong(getMostSignificantBits());
+        buf.putLong(getLeastSignificantBits());
+        return bytes;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Decimal128 that = (Decimal128) o;
+
+        if (lsb != that.lsb) {
+            return false;
+        }
+        if (msb != that.msb) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (msb ^ (msb >>> 32));
+        result = 31 * result + (int) (lsb ^ (lsb >>> 32));
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal32.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal32.java
new file mode 100644
index 0000000..d09f516
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal32.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import java.lang.annotation.Native;
+import java.math.BigDecimal;
+
+public final class Decimal32 extends Number {
+
+    private static final long serialVersionUID = 1404882516677613318L;
+
+    /**
+     * The number of bits used to represent an {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    @Native public static final int SIZE = 32;
+
+    /**
+     * The number of bytes used to represent a {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    public static final int BYTES = SIZE / Byte.SIZE;
+
+    private final BigDecimal underlying;
+    private final int bits;
+
+    public Decimal32(BigDecimal underlying) {
+        this.underlying = underlying;
+        this.bits = calculateBits(underlying);
+    }
+
+    public Decimal32(final int bits) {
+        this.bits = bits;
+        this.underlying = calculateBigDecimal(bits);
+    }
+
+    static int calculateBits(final BigDecimal underlying) {
+        return 0; // TODO.
+    }
+
+    static BigDecimal calculateBigDecimal(int bits) {
+        return BigDecimal.ZERO; // TODO
+    }
+
+    @Override
+    public int intValue() {
+        return underlying.intValue();
+    }
+
+    @Override
+    public long longValue() {
+        return underlying.longValue();
+    }
+
+    @Override
+    public float floatValue() {
+        return underlying.floatValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return underlying.doubleValue();
+    }
+
+    public int getBits() {
+        return bits;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Decimal32 decimal32 = (Decimal32) o;
+
+        if (bits != decimal32.bits) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return bits;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal64.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal64.java
new file mode 100644
index 0000000..0b0b239
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Decimal64.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import java.lang.annotation.Native;
+import java.math.BigDecimal;
+
+public final class Decimal64 extends Number {
+
+    private static final long serialVersionUID = -3811100445576755687L;
+
+    /**
+     * The number of bits used to represent an {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    @Native public static final int SIZE = 64;
+
+    /**
+     * The number of bytes used to represent a {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    public static final int BYTES = SIZE / Byte.SIZE;
+
+    private final BigDecimal underlying;
+    private final long bits;
+
+    public Decimal64(BigDecimal underlying) {
+        this.underlying = underlying;
+        this.bits = calculateBits(underlying);
+    }
+
+    public Decimal64(final long bits) {
+        this.bits = bits;
+        this.underlying = calculateBigDecimal(bits);
+    }
+
+    static BigDecimal calculateBigDecimal(final long bits) {
+        return BigDecimal.ZERO;
+    }
+
+    static long calculateBits(final BigDecimal underlying) {
+        return 0l; // TODO
+    }
+
+    @Override
+    public int intValue() {
+        return underlying.intValue();
+    }
+
+    @Override
+    public long longValue() {
+        return underlying.longValue();
+    }
+
+    @Override
+    public float floatValue() {
+        return underlying.floatValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return underlying.doubleValue();
+    }
+
+    public long getBits() {
+        return bits;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Decimal64 decimal64 = (Decimal64) o;
+
+        if (bits != decimal64.bits) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (bits ^ (bits >>> 32));
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/DescribedType.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/DescribedType.java
new file mode 100644
index 0000000..47a2ec9
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/DescribedType.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+public interface DescribedType {
+
+    /**
+     * Returns the Described Type descriptor that identified this instance.
+     *
+     * @return the descriptor that identifies this instance.
+     */
+    public Object getDescriptor();
+
+    /**
+     * Returns the described type value that is carried in this instance.
+     *
+     * @return the value carried inside this described instance.
+     */
+    public Object getDescribed();
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Symbol.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Symbol.java
new file mode 100644
index 0000000..8072197
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/Symbol.java
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+// TODO: Incomplete Symbol class which should be simplified for the Data codec
+public final class Symbol implements Comparable<Symbol> {
+
+    private static final Map<ByteBuffer, Symbol> bufferToSymbols = new ConcurrentHashMap<>(2048);
+    private static final Map<String, Symbol> stringToSymbols = new ConcurrentHashMap<>(2048);
+
+    private static final Symbol EMPTY_SYMBOL = new Symbol();
+
+    private static final int MAX_CACHED_SYMBOL_SIZE = 64;
+
+    private String symbolString;
+    private final ByteBuffer underlying;
+    private final int hashCode;
+
+    private Symbol() {
+        this.underlying = ByteBuffer.allocate(0);
+        this.hashCode = 31;
+        this.symbolString = "";
+    }
+
+    private Symbol(ByteBuffer underlying) {
+        this.underlying = underlying;
+        this.hashCode = underlying.hashCode();
+    }
+
+    public int getLength() {
+        return underlying.remaining();
+    }
+
+    @Override
+    public int compareTo(Symbol other) {
+        return underlying.compareTo(other.underlying);
+    }
+
+    @Override
+    public String toString() {
+        if (symbolString == null && underlying.remaining() > 0) {
+            symbolString = new String(underlying.array(), underlying.arrayOffset(), underlying.remaining(), StandardCharsets.US_ASCII);
+
+            if (underlying.remaining() <= MAX_CACHED_SYMBOL_SIZE) {
+                final Symbol existing;
+                if ((existing = stringToSymbols.putIfAbsent(symbolString, this)) != null) {
+                    symbolString = existing.symbolString;
+                }
+            }
+        }
+
+        return symbolString;
+    }
+
+    @Override
+    public int hashCode() {
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof Symbol) {
+            return underlying.equals(((Symbol) other).underlying);
+        }
+
+        return false;
+    }
+
+    public void writeTo(ByteBuffer target) {
+        for (int i = 0; i < underlying.remaining(); ++i) {
+            target.put(underlying.get(i));
+        }
+    }
+
+    public static Symbol valueOf(String symbolVal) {
+        return getSymbol(symbolVal);
+    }
+
+    public static Symbol getSymbol(ByteBuffer symbolBytes) {
+        return getSymbol(symbolBytes, false);
+    }
+
+    public static Symbol getSymbol(ByteBuffer symbolBuffer, boolean copyOnCreate) {
+        if (symbolBuffer == null) {
+            return null;
+        } else if (!symbolBuffer.hasRemaining()) {
+            return EMPTY_SYMBOL;
+        }
+
+        Symbol symbol = bufferToSymbols.get(symbolBuffer);
+        if (symbol == null) {
+            if (copyOnCreate) {
+                // Copy to a known heap based buffer to avoid issue with life-cycle of pooled buffer types.
+                int symbolSize = symbolBuffer.remaining();
+                byte[] copy = new byte[symbolSize];
+                symbolBuffer.get(copy, 0, symbolSize);
+                symbolBuffer.position(symbolBuffer.position() - symbolSize);
+                symbolBuffer = ByteBuffer.wrap(copy);
+            }
+
+            symbol = new Symbol(symbolBuffer);
+
+            // Don't cache overly large symbols to prevent holding large
+            // amount of memory in the symbol cache.
+            if (symbolBuffer.remaining() <= MAX_CACHED_SYMBOL_SIZE) {
+                final Symbol existing;
+                if ((existing = bufferToSymbols.putIfAbsent(symbolBuffer, symbol)) != null) {
+                    symbol = existing;
+                }
+            }
+        }
+
+        return symbol;
+    }
+
+    public static Symbol getSymbol(String stringValue) {
+        if (stringValue == null) {
+            return null;
+        } else if (stringValue.isEmpty()) {
+            return EMPTY_SYMBOL;
+        }
+
+        Symbol symbol = stringToSymbols.get(stringValue);
+        if (symbol == null) {
+            symbol = getSymbol(ByteBuffer.wrap(stringValue.getBytes(US_ASCII)));
+
+            // Don't cache overly large symbols to prevent holding large
+            // amount of memory in the symbol cache.
+            if (symbol.underlying.remaining() <= MAX_CACHED_SYMBOL_SIZE) {
+                stringToSymbols.put(stringValue, symbol);
+            }
+        }
+
+        return symbol;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnknownDescribedType.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnknownDescribedType.java
new file mode 100644
index 0000000..30109bb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnknownDescribedType.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+public class UnknownDescribedType implements DescribedType {
+
+    private final Object descriptor;
+    private final Object described;
+
+    public UnknownDescribedType(final Object descriptor, final Object described) {
+        this.descriptor = descriptor;
+        this.described = described;
+    }
+
+    @Override
+    public Object getDescriptor() {
+        return descriptor;
+    }
+
+    @Override
+    public Object getDescribed() {
+        return described;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final UnknownDescribedType that = (UnknownDescribedType) o;
+
+        if (described != null ? !described.equals(that.described) : that.described != null) {
+            return false;
+        }
+        if (descriptor != null ? !descriptor.equals(that.descriptor) : that.descriptor != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = descriptor != null ? descriptor.hashCode() : 0;
+        result = 31 * result + (described != null ? described.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "UnknownDescribedType{" + "descriptor=" + descriptor + ", described=" + described + '}';
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedByte.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedByte.java
new file mode 100644
index 0000000..33369e0
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedByte.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+public final class UnsignedByte extends Number implements Comparable<UnsignedByte> {
+
+    private static final long serialVersionUID = 6271683731751283403L;
+    private static final UnsignedByte[] cachedValues = new UnsignedByte[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedByte((byte) i);
+        }
+    }
+
+    private final byte underlying;
+
+    public UnsignedByte(byte underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public byte byteValue() {
+        return underlying;
+    }
+
+    @Override
+    public short shortValue() {
+        return (short) intValue();
+    }
+
+    @Override
+    public int intValue() {
+        return (underlying) & 0xFF;
+    }
+
+    @Override
+    public long longValue() {
+        return (underlying) & 0xFFl;
+    }
+
+    @Override
+    public float floatValue() {
+        return longValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return longValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedByte that = (UnsignedByte) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give byte value to this unsigned byte numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the byte to compare to this unsigned byte instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(byte value) {
+        return compare(underlying, value);
+    }
+
+    @Override
+    public int compareTo(UnsignedByte o) {
+        return compare(underlying, o.underlying);
+    }
+
+    /**
+     * Compares two short values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side short to compare
+     * @param right
+     *       the right hand side short to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(byte left, byte right) {
+        return Integer.compareUnsigned(Byte.toUnsignedInt(left), Byte.toUnsignedInt(right));
+    }
+
+    @Override
+    public int hashCode() {
+        return underlying;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(intValue());
+    }
+
+    /**
+     * Returns an UnsignedByte instance representing the specified byte value. This method always returns
+     * a cached {@link UnsignedByte} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedByte#UnsignedByte(byte)} which will always create a new
+     * instance of the {@link UnsignedByte} type.
+     *
+     * @param value
+     *      The byte value to return as an {@link UnsignedByte} instance.
+     *
+     * @return an {@link UnsignedByte} instance representing the unsigned view of the given byte.
+     */
+    public static UnsignedByte valueOf(byte value) {
+        final int index = (value) & 0xFF;
+        return cachedValues[index];
+    }
+
+    /**
+     * Returns an {@link UnsignedByte} instance representing the specified {@link String} value. This method always
+     * returns a cached {@link UnsignedByte} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedByte#UnsignedByte(byte)} which will always create a new instance
+     * of the {@link UnsignedByte} type.
+     *
+     * @param value
+     *      The byte value to return as an {@link UnsignedByte} instance.
+     *
+     * @return an {@link UnsignedByte} instance representing the unsigned view of the given byte.
+     *
+     * @throws NumberFormatException if the given {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedByte valueOf(final String value) throws NumberFormatException {
+        int intVal = Integer.parseInt(value);
+        if (intVal < 0 || intVal >= (1 << 8)) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [" + 0 + "-" + (1 << 8) + ").");
+        }
+        return valueOf((byte) intVal);
+    }
+
+    /**
+     * Returns a {@code short} that represents the unsigned view of the given {@code byte} value.
+     *
+     * @param value
+     *      The {@code short} whose unsigned value should be converted to a long.
+     *
+     * @return a positive {@code short} value that represents the given {@code byte} as unsigned.
+     */
+    public static short toUnsignedShort(byte value) {
+        return (short) (value & 0xff);
+    }
+
+    /**
+     * Returns a {@code int} that represents the unsigned view of the given {@code byte} value.
+     *
+     * @param value
+     *      The {@code int} whose unsigned value should be converted to a long.
+     *
+     * @return a positive {@code int} value that represents the given {@code short} as unsigned.
+     */
+    public static int toUnsignedInt(byte value) {
+        return Byte.toUnsignedInt(value);
+    }
+
+    /**
+     * Returns a {@code long} that represents the unsigned view of the given {@code byte} value.
+     *
+     * @param value
+     *      The {@code long} whose unsigned value should be converted to a long.
+     *
+     * @return a positive {@code long} value that represents the given {@code byte} as unsigned.
+     */
+    public static long toUnsignedLong(byte value) {
+        return Byte.toUnsignedLong(value);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedInteger.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedInteger.java
new file mode 100644
index 0000000..b4dd1bb
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedInteger.java
@@ -0,0 +1,246 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+public final class UnsignedInteger extends Number implements Comparable<UnsignedInteger> {
+
+    private static final long serialVersionUID = 3042749852724499995L;
+    private static final UnsignedInteger[] cachedValues = new UnsignedInteger[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedInteger(i);
+        }
+    }
+
+    public static final UnsignedInteger ZERO = cachedValues[0];
+    public static final UnsignedInteger ONE = cachedValues[1];
+    public static final UnsignedInteger MAX_VALUE = new UnsignedInteger(0xffffffff);
+
+    private final int underlying;
+
+    public UnsignedInteger(int underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public int intValue() {
+        return underlying;
+    }
+
+    @Override
+    public long longValue() {
+        return Integer.toUnsignedLong(underlying);
+    }
+
+    @Override
+    public float floatValue() {
+        return longValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return longValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedInteger that = (UnsignedInteger) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give integer value to this unsigned integer numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the integer to compare to this unsigned integer instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(int value) {
+        return Integer.compareUnsigned(underlying, value);
+    }
+
+    /**
+     * Compares the give long value to this unsigned integer numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the long to compare to this unsigned integer instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(long value) {
+        return Long.compareUnsigned(longValue(), value);
+    }
+
+    @Override
+    public int compareTo(UnsignedInteger value) {
+        return Long.compareUnsigned(longValue(), value.longValue());
+    }
+
+    /**
+     * Compares two integer values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side integer to compare
+     * @param right
+     *       the right hand side integer to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(int left, int right) {
+        return Integer.compareUnsigned(left, right);
+    }
+
+    /**
+     * Compares two long values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side long value to compare
+     * @param right
+     *       the right hand side long value to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(long left, long right) {
+        return Long.compareUnsigned(left, right);
+    }
+
+    @Override
+    public int hashCode() {
+        return underlying;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(longValue());
+    }
+
+    /**
+     * Returns an UnsignedInteger instance representing the specified int value. This method always returns
+     * a cached {@link UnsignedInteger} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedInteger#UnsignedInteger(int)} which will always create a new
+     * instance of the {@link UnsignedInteger} type.
+     *
+     * @param value
+     *      The int value to return as an {@link UnsignedInteger} instance.
+     *
+     * @return an {@link UnsignedInteger} instance representing the unsigned view of the given int.
+     */
+    public static UnsignedInteger valueOf(int value) {
+        if ((value & 0xFFFFFF00) == 0) {
+            return cachedValues[value];
+        } else {
+            return new UnsignedInteger(value);
+        }
+    }
+
+    /**
+     * Adds the value of the given {@link UnsignedInteger} to this instance and return a new {@link UnsignedInteger}
+     * instance that represents the newly computed value.
+     *
+     * @param value
+     *      The {@link UnsignedInteger} whose underlying value should be added to this instance's value.
+     *
+     * @return a new immutable {@link UnsignedInteger} resulting from the addition of this with the given value.
+     */
+    public UnsignedInteger add(final UnsignedInteger value) {
+        int val = underlying + value.underlying;
+        return UnsignedInteger.valueOf(val);
+    }
+
+    /**
+     * Subtract the value of the given {@link UnsignedInteger} from this instance and return a new {@link UnsignedInteger}
+     * instance that represents the newly computed value.
+     *
+     * @param value
+     *      The {@link UnsignedInteger} whose underlying value should be subtracted to this instance's value.
+     *
+     * @return a new immutable {@link UnsignedInteger} resulting from the subtraction the given value from this.
+     */
+    public UnsignedInteger subtract(final UnsignedInteger value) {
+        int val = underlying - value.underlying;
+        return UnsignedInteger.valueOf(val);
+    }
+
+    /**
+     * Returns an {@link UnsignedInteger} instance representing the specified {@link String} value. This method
+     * always returns a cached {@link UnsignedInteger} instance for values in the range [0...255] which can save
+     * space and time over calling the constructor {@link UnsignedInteger#UnsignedInteger(int)} which will always
+     * create a new instance of the {@link UnsignedInteger} type.
+     *
+     * @param value
+     *      The String value to return as an {@link UnsignedInteger} instance.
+     *
+     * @return an {@link UnsignedInteger} instance representing the unsigned view of the given String.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedInteger} value possible
+     *                               or the {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedInteger valueOf(final String value) {
+        long longVal = Long.parseLong(value);
+        return valueOf(longVal);
+    }
+
+    /**
+     * Returns an UnsignedInteger instance representing the specified long value. This method always returns
+     * a cached {@link UnsignedInteger} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedInteger#UnsignedInteger(int)} which will always create a new
+     * instance of the {@link UnsignedInteger} type.
+     *
+     * @param value
+     *      The long value to return as an {@link UnsignedInteger} instance.
+     *
+     * @return an {@link UnsignedInteger} instance representing the unsigned view of the given long.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedInteger} value possible.
+     */
+    public static UnsignedInteger valueOf(final long value) {
+        if (value < 0L || value >= (1L << 32)) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [" + 0L + "-" + (1L << 32) + ").");
+        }
+        return valueOf((int) value);
+    }
+
+    /**
+     * Returns a {@code long} that represents the unsigned view of the given {@code int} value.
+     *
+     * @param value
+     *      The integer whose unsigned value should be converted to a long.
+     *
+     * @return a positive long value that represents the given {@code int} as unsigned.
+     */
+    public static long toUnsignedLong(int value) {
+        return Integer.toUnsignedLong(value);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedLong.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedLong.java
new file mode 100644
index 0000000..63fe45c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedLong.java
@@ -0,0 +1,194 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import java.math.BigInteger;
+
+public final class UnsignedLong extends Number implements Comparable<UnsignedLong> {
+
+    private static final long serialVersionUID = -5901821450224443596L;
+    private static final BigInteger TWO_TO_THE_SIXTY_FOUR = new BigInteger(
+        new byte[] { (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+    private static final BigInteger LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
+
+    private static final UnsignedLong[] cachedValues = new UnsignedLong[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedLong(i);
+        }
+    }
+
+    public static final UnsignedLong ZERO = cachedValues[0];
+
+    private final long underlying;
+
+    public UnsignedLong(long underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public int intValue() {
+        return (int) underlying;
+    }
+
+    @Override
+    public long longValue() {
+        return underlying;
+    }
+
+    public BigInteger bigIntegerValue() {
+        if (underlying >= 0L) {
+            return BigInteger.valueOf(underlying);
+        } else {
+            return TWO_TO_THE_SIXTY_FOUR.add(BigInteger.valueOf(underlying));
+        }
+    }
+
+    @Override
+    public float floatValue() {
+        return longValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return longValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedLong that = (UnsignedLong) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give long value to this {@link UnsignedLong} numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the long to compare to this {@link UnsignedLong} instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(long value) {
+        return Long.compareUnsigned(underlying, value);
+    }
+
+    @Override
+    public int compareTo(UnsignedLong o) {
+        return bigIntegerValue().compareTo(o.bigIntegerValue());
+    }
+
+    /**
+     * Compares two long values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side long to compare
+     * @param right
+     *       the right hand side long to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(long left, long right) {
+        return Long.compareUnsigned(left, right);
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (underlying ^ (underlying >>> 32));
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(bigIntegerValue());
+    }
+
+    /**
+     * Returns an UnsignedLong instance representing the specified int value. This method always returns
+     * a cached {@link UnsignedLong} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedLong#UnsignedLong(long)} which will always create a new
+     * instance of the {@link UnsignedLong} type.
+     *
+     * @param value
+     *      The long value to return as an {@link UnsignedLong} instance.
+     *
+     * @return an {@link UnsignedLong} instance representing the unsigned view of the given long.
+     */
+    public static UnsignedLong valueOf(long value) {
+        if ((value & 0xFFL) == value) {
+            return cachedValues[(int) value];
+        } else {
+            return new UnsignedLong(value);
+        }
+    }
+
+    /**
+     * Returns an {@link UnsignedLong} instance representing the specified {@link String} value. This method
+     * always returns a cached {@link UnsignedLong} instance for values in the range [0...255] which can save
+     * space and time over calling the constructor {@link UnsignedLong#UnsignedLong(long)} which will always
+     * create a new instance of the {@link UnsignedLong} type.
+     *
+     * @param value
+     *      The String value to return as an {@link UnsignedLong} instance.
+     *
+     * @return an {@link UnsignedLong} instance representing the unsigned view of the given String.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedLong} value possible
+     *                               or the {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedLong valueOf(final String value) {
+        BigInteger bigInt = new BigInteger(value);
+
+        return valueOf(bigInt);
+    }
+
+    /**
+     * Returns an {@link UnsignedLong} instance representing the specified {@link BigInteger} value. This method
+     * always returns a cached {@link UnsignedLong} instance for values in the range [0...255] which can save
+     * space and time over calling the constructor {@link UnsignedLong#UnsignedLong(long)} which will always
+     * create a new instance of the {@link UnsignedLong} type.
+     *
+     * @param value
+     *      The {@link BigInteger} value to return as an {@link UnsignedLong} instance.
+     *
+     * @return an {@link UnsignedLong} instance representing the unsigned view of the given {@link BigInteger}.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedLong} value possible.
+     */
+    public static UnsignedLong valueOf(BigInteger value) {
+        if (value.signum() == -1 || value.bitLength() > 64) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [0 - 2^64).");
+        } else if (value.compareTo(LONG_MAX_VALUE) >= 0) {
+            return UnsignedLong.valueOf(value.longValue());
+        } else {
+            return UnsignedLong.valueOf(TWO_TO_THE_SIXTY_FOUR.subtract(value).negate().longValue());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedShort.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedShort.java
new file mode 100644
index 0000000..007cc12
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedShort.java
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+public final class UnsignedShort extends Number implements Comparable<UnsignedShort> {
+
+    private static final long serialVersionUID = 6006944990203315231L;
+    private static final UnsignedShort[] cachedValues = new UnsignedShort[256];
+
+    static {
+        for (short i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedShort(i);
+        }
+    }
+
+    public static final UnsignedShort MAX_VALUE = new UnsignedShort((short) -1);
+
+    private final short underlying;
+
+    public UnsignedShort(short underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public short shortValue() {
+        return underlying;
+    }
+
+    @Override
+    public int intValue() {
+        return Short.toUnsignedInt(underlying);
+    }
+
+    @Override
+    public long longValue() {
+        return Short.toUnsignedLong(underlying);
+    }
+
+    @Override
+    public float floatValue() {
+        return intValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return intValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedShort that = (UnsignedShort) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give short value to this unsigned short numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the short to compare to this unsigned short instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(short value) {
+        return Integer.signum(intValue() - Short.toUnsignedInt(value));
+    }
+
+    @Override
+    public int compareTo(UnsignedShort value) {
+        return Integer.signum(intValue() - value.intValue());
+    }
+
+    /**
+     * Compares two short values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side short to compare
+     * @param right
+     *       the right hand side short to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(short left, short right) {
+        return Integer.compareUnsigned(Short.toUnsignedInt(left), Short.toUnsignedInt(right));
+    }
+
+    @Override
+    public int hashCode() {
+        return underlying;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(longValue());
+    }
+
+    /**
+     * Returns an UnsignedShort instance representing the specified short value. This method always returns
+     * a cached {@link UnsignedShort} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedShort#UnsignedShort(short)} which will always create a new
+     * instance of the {@link UnsignedShort} type.
+     *
+     * @param value
+     *      The short value to return as an {@link UnsignedShort} instance.
+     *
+     * @return an {@link UnsignedShort} instance representing the unsigned view of the given short.
+     */
+    public static UnsignedShort valueOf(final short value) {
+        if ((value & 0xFF00) == 0) {
+            return cachedValues[value];
+        } else {
+            return new UnsignedShort(value);
+        }
+    }
+
+    /**
+     * Returns an UnsignedShort instance representing the specified int value. This method always returns
+     * a cached {@link UnsignedShort} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedShort#UnsignedShort(short)} which will always create a new
+     * instance of the {@link UnsignedShort} type.
+     *
+     * @param value
+     *      The short value to return as an {@link UnsignedShort} instance.
+     *
+     * @return an {@link UnsignedShort} instance representing the unsigned view of the given short.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedShort} value possible.
+     */
+    public static UnsignedShort valueOf(final int value) {
+        if (value < 0L || value >= (1L << 16)) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [" + 0L + "-" + (1L << 16) + ").");
+        }
+        return valueOf((short) value);
+    }
+
+    /**
+     * Returns an UnsignedShort instance representing the specified {@link String} value. This method always returns
+     * a cached {@link UnsignedShort} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedShort#UnsignedShort(short)} which will always create a new
+     * instance of the {@link UnsignedShort} type.
+     *
+     * @param value
+     *      The String value to return as an {@link UnsignedShort} instance.
+     *
+     * @return an {@link UnsignedShort} instance representing the unsigned view of the given String.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedShort} value possible
+     *                               or the {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedShort valueOf(final String value) {
+        int intVal = Integer.parseInt(value);
+
+        if (intVal < 0 || intVal >= (1 << 16)) {
+            throw new NumberFormatException(
+                "Value \"" + value + "\" lies outside the range [" + 0 + "-" + (1 << 16) + ").");
+        }
+
+        return valueOf((short) intVal);
+    }
+
+    /**
+     * Returns a {@code int} that represents the unsigned view of the given {@code short} value.
+     *
+     * @param value
+     *      The integer whose unsigned value should be converted to a long.
+     *
+     * @return a positive long value that represents the given {@code short} as unsigned.
+     */
+    public static int toUnsignedInt(short value) {
+        return Short.toUnsignedInt(value);
+    }
+
+    /**
+     * Returns a {@code long} that represents the unsigned view of the given {@code short} value.
+     *
+     * @param value
+     *      The integer whose unsigned value should be converted to a long.
+     *
+     * @return a positive long value that represents the given {@code short} as unsigned.
+     */
+    public static long toUnsignedLong(short value) {
+        return Short.toUnsignedLong(value);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslChallenge.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslChallenge.java
new file mode 100644
index 0000000..3ff3528
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslChallenge.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class SaslChallenge extends SaslDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-challenge:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000042L);
+
+    /**
+     * Enumeration which maps to fields in the SaslChallenge Performative
+     */
+    public enum Field {
+        CHALLENGE,
+    }
+
+    public SaslChallenge() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public SaslChallenge(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public SaslChallenge(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public SaslChallenge setChallenge(Binary o) {
+        getList().set(Field.CHALLENGE.ordinal(), o);
+        return this;
+    }
+
+    public Binary getChallenge() {
+        return (Binary) getList().get(Field.CHALLENGE.ordinal());
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.CHALLENGE;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleChallenge(this, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslCode.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslCode.java
new file mode 100644
index 0000000..526b69b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslCode.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+
+public enum SaslCode {
+
+    OK, AUTH, SYS, SYS_PERM, SYS_TEMP;
+
+    public UnsignedByte getValue() {
+        return UnsignedByte.valueOf((byte) ordinal());
+    }
+
+    public static SaslCode valueOf(byte v) {
+        return SaslCode.values()[v];
+    }
+
+    public static SaslCode valueOf(UnsignedByte v) {
+        return SaslCode.values()[v.byteValue()];
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslDescribedType.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslDescribedType.java
new file mode 100644
index 0000000..9ea708f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslDescribedType.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+
+/**
+ * SASL Types base class used to mark types that are SASL related
+ */
+public abstract class SaslDescribedType extends ListDescribedType {
+
+    public enum SaslPerformativeType {
+        INIT,
+        MECHANISMS,
+        CHALLENGE,
+        RESPONSE,
+        OUTCOME
+    }
+
+    public SaslDescribedType(int numberOfFields) {
+        super(numberOfFields);
+    }
+
+    public SaslDescribedType(int numberOfFields, List<Object> described) {
+        super(numberOfFields, described);
+    }
+
+    public abstract SaslPerformativeType getPerformativeType();
+
+    public interface SaslPerformativeHandler<E> {
+
+        default void handleMechanisms(SaslMechanisms saslMechanisms, E context) {
+            throw new AssertionError("SASL Mechanisms was not handled");
+        }
+        default void handleInit(SaslInit saslInit, E context) {
+            throw new AssertionError("SASL Init was not handled");
+        }
+        default void handleChallenge(SaslChallenge saslChallenge, E context) {
+            throw new AssertionError("SASL Challenge was not handled");
+        }
+        default void handleResponse(SaslResponse saslResponse, E context) {
+            throw new AssertionError("SASL Response was not handled");
+        }
+        default void handleOutcome(SaslOutcome saslOutcome, E context) {
+            throw new AssertionError("SASL Outcome was not handled");
+        }
+    }
+
+    public abstract <E> void invoke(SaslPerformativeHandler<E> handler, E context);
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslInit.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslInit.java
new file mode 100644
index 0000000..25b9ea3
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslInit.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class SaslInit extends SaslDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-init:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000041L);
+
+    /**
+     * Enumeration which maps to fields in the SaslInit Performative
+     */
+    public enum Field {
+        MECHANISM,
+        INITIAL_RESPONSE,
+        HOSTNAME
+    }
+
+    public SaslInit() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public SaslInit(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public SaslInit(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public SaslInit setMechanism(Symbol o) {
+        getList().set(Field.MECHANISM.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getMechanism() {
+        return (Symbol) getList().get(Field.MECHANISM.ordinal());
+    }
+
+    public SaslInit setInitialResponse(Binary o) {
+        getList().set(Field.INITIAL_RESPONSE.ordinal(), o);
+        return this;
+    }
+
+    public Binary getInitialResponse() {
+        return (Binary) getList().get(Field.INITIAL_RESPONSE.ordinal());
+    }
+
+    public SaslInit setHostname(String o) {
+        getList().set(Field.HOSTNAME.ordinal(), o);
+        return this;
+    }
+
+    public String getHostname() {
+        return (String) getList().get(Field.HOSTNAME.ordinal());
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.INIT;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleInit(this, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslMechanisms.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslMechanisms.java
new file mode 100644
index 0000000..29e12cd
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslMechanisms.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class SaslMechanisms extends SaslDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-mechanisms:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000040L);
+
+    /**
+     * Enumeration which maps to fields in the SaslMechanisms Performative
+     */
+    public enum Field {
+        SASL_SERVER_MECHANISMS,
+    }
+
+    public SaslMechanisms() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public SaslMechanisms(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public SaslMechanisms(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public SaslMechanisms setSaslServerMechanisms(Symbol... o) {
+        getList().set(Field.SASL_SERVER_MECHANISMS.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getSaslServerMechanisms() {
+        return (Symbol[]) getList().get(Field.SASL_SERVER_MECHANISMS.ordinal());
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.MECHANISMS;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleMechanisms(this, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslOutcome.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslOutcome.java
new file mode 100644
index 0000000..3092043
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslOutcome.java
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class SaslOutcome extends SaslDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-outcome:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000044L);
+
+    /**
+     * Enumeration which maps to fields in the Rejected Performative
+     */
+    public enum Field {
+        CODE,
+        ADDITIONAL_DATA
+    }
+
+    public SaslOutcome() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public SaslOutcome(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public SaslOutcome(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public UnsignedLong getDescriptor() {
+        return DESCRIPTOR_CODE;
+    }
+
+    public SaslOutcome setCode(UnsignedByte o) {
+        getList().set(Field.CODE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedByte getCode() {
+        return (UnsignedByte) getList().get(Field.CODE.ordinal());
+    }
+
+    public SaslOutcome setAdditionalData(Binary o) {
+        getList().set(Field.ADDITIONAL_DATA.ordinal(), o);
+        return this;
+    }
+
+    public Binary getAdditionalData() {
+        return (Binary) getList().get(Field.ADDITIONAL_DATA.ordinal());
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.OUTCOME;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleOutcome(this, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslResponse.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslResponse.java
new file mode 100644
index 0000000..2b7c7d9
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/security/SaslResponse.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.security;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class SaslResponse extends SaslDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-response:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000043L);
+
+    /**
+     * Enumeration which maps to fields in the SaslResponse Performative
+     */
+    public enum Field {
+        RESPONSE,
+    }
+
+    public SaslResponse() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public SaslResponse(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public SaslResponse(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public SaslResponse setResponse(Binary o) {
+        getList().set(Field.RESPONSE.ordinal(), o);
+        return this;
+    }
+
+    public Binary getResponse() {
+        return (Binary) getList().get(Field.RESPONSE.ordinal());
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.RESPONSE;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleResponse(this, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Coordinator.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Coordinator.java
new file mode 100644
index 0000000..244bcb2
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Coordinator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transactions;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Coordinator extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:coordinator:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000030L);
+
+    /**
+     * Enumeration which maps to fields in the Coordinator Performative
+     */
+    public enum Field {
+        CAPABILITIES
+    }
+
+    public Coordinator() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Coordinator(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Coordinator(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Coordinator setCapabilities(Symbol... o) {
+        getList().set(Field.CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getCapabilities() {
+        return (Symbol[]) getList().get(Field.CAPABILITIES.ordinal());
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Declare.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Declare.java
new file mode 100644
index 0000000..7dd383f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Declare.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transactions;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Declare extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:declare:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000031L);
+
+    /**
+     * Enumeration which maps to fields in the Declare Performative
+     */
+    public enum Field {
+        GLOBAL_ID
+    }
+
+    public Declare() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Declare(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Declare(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Declare setGlobalId(Binary o) {
+        getList().set(Field.GLOBAL_ID.ordinal(), o);
+        return this;
+    }
+
+    public Binary getGlobalId() {
+        return (Binary) getList().get(Field.GLOBAL_ID.ordinal());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Declared.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Declared.java
new file mode 100644
index 0000000..fc2026c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Declared.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transactions;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Outcome;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+
+public class Declared extends ListDescribedType implements DeliveryState, Outcome {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:declared:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000033L);
+
+    /**
+     * Enumeration which maps to fields in the Declared Performative
+     */
+    public enum Field {
+        TXN_ID
+    }
+
+    public Declared() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Declared(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Declared(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Declared setTxnId(Binary o) {
+        getList().set(Field.TXN_ID.ordinal(), o);
+        return this;
+    }
+
+    public Binary getTxnId() {
+        return (Binary) getList().get(Field.TXN_ID.ordinal());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Declared;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Discharge.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Discharge.java
new file mode 100644
index 0000000..9cf4a2b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/Discharge.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transactions;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class Discharge extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:discharge:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000032L);
+
+    /**
+     * Enumeration which maps to fields in the Discharge Performative
+     */
+    public enum Field {
+        TXN_ID,
+        FAIL
+    }
+
+    public Discharge() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Discharge(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Discharge(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Discharge setTxnId(Binary o) {
+        getList().set(Field.TXN_ID.ordinal(), o);
+        return this;
+    }
+
+    public Binary getTxnId() {
+        return (Binary) getList().get(Field.TXN_ID.ordinal());
+    }
+
+    public Discharge setFail(Boolean o) {
+        getList().set(Field.FAIL.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getFail() {
+        return (Boolean) getList().get(Field.FAIL.ordinal());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/TransactionalState.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/TransactionalState.java
new file mode 100644
index 0000000..ebbef81
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transactions/TransactionalState.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transactions;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+
+public class TransactionalState extends ListDescribedType implements DeliveryState {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:transactional-state:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000034L);
+
+    /**
+     * Enumeration which maps to fields in the TransactionalState Performative
+     */
+    public enum Field {
+        TXN_ID,
+        OUTCOME
+    }
+
+    public TransactionalState() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public TransactionalState(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public TransactionalState(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public TransactionalState setTxnId(Binary o) {
+        getList().set(Field.TXN_ID.ordinal(), o);
+        return this;
+    }
+
+    public Binary getTxnId() {
+        return (Binary) getList().get(Field.TXN_ID.ordinal());
+    }
+
+    public TransactionalState setOutcome(DescribedType o) {
+        getList().set(Field.OUTCOME.ordinal(), o);
+        return this;
+    }
+
+    public DescribedType getOutcome() {
+        return (DescribedType) getList().get(Field.OUTCOME.ordinal());
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Transactional;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/AMQPHeader.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/AMQPHeader.java
new file mode 100644
index 0000000..bc9646b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/AMQPHeader.java
@@ -0,0 +1,335 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Represents the AMQP protocol handshake packet that is sent during the
+ * initial exchange with a remote peer.
+ */
+public final class AMQPHeader {
+
+    static final byte[] PREFIX = new byte[] { 'A', 'M', 'Q', 'P' };
+
+    public static final int PROTOCOL_ID_INDEX = 4;
+    public static final int MAJOR_VERSION_INDEX = 5;
+    public static final int MINOR_VERSION_INDEX = 6;
+    public static final int REVISION_INDEX = 7;
+
+    public static final byte AMQP_PROTOCOL_ID = 0;
+    public static final byte SASL_PROTOCOL_ID = 3;
+
+    public static final int HEADER_SIZE_BYTES = 8;
+
+    private static final AMQPHeader AMQP_HEADER =
+        new AMQPHeader(new byte[] { 'A', 'M', 'Q', 'P', 0, 1, 0, 0 });
+
+    private static final AMQPHeader SASL_HEADER =
+        new AMQPHeader(new byte[] { 'A', 'M', 'Q', 'P', 3, 1, 0, 0 });
+
+    private byte[] buffer;
+
+    public AMQPHeader() {
+        this(AMQP_HEADER.buffer);
+    }
+
+    public AMQPHeader(byte[] headerBytes) {
+        setBuffer(Arrays.copyOf(headerBytes, headerBytes.length), true);
+    }
+
+    public AMQPHeader(byte[] headerBytes, boolean validate) {
+        setBuffer(Arrays.copyOf(headerBytes, headerBytes.length), validate);
+    }
+
+    public AMQPHeader(ByteBuffer buffer) {
+        ByteBuffer duplicate = ByteBuffer.allocate(HEADER_SIZE_BYTES);
+        buffer.get(duplicate.array(), 0, HEADER_SIZE_BYTES);
+        setBuffer(duplicate.array(), true);
+    }
+
+    public AMQPHeader(ByteBuffer buffer, boolean validate) {
+        ByteBuffer duplicate = ByteBuffer.allocate(HEADER_SIZE_BYTES);
+        buffer.get(duplicate.array(), 0, HEADER_SIZE_BYTES);
+        setBuffer(duplicate.array(), validate);
+    }
+
+    public static AMQPHeader getAMQPHeader() {
+        return AMQP_HEADER;
+    }
+
+    public static AMQPHeader getSASLHeader() {
+        return SASL_HEADER;
+    }
+
+    public int getProtocolId() {
+        return buffer[PROTOCOL_ID_INDEX] & 0xFF;
+    }
+
+    public int getMajor() {
+        return buffer[MAJOR_VERSION_INDEX] & 0xFF;
+    }
+
+    public int getMinor() {
+        return buffer[MINOR_VERSION_INDEX] & 0xFF;
+    }
+
+    public int getRevision() {
+        return buffer[REVISION_INDEX] & 0xFF;
+    }
+
+    public byte[] getBuffer() {
+        return Arrays.copyOf(buffer, buffer.length);
+    }
+
+    public byte[] toArray() {
+        if (buffer != null) {
+            return Arrays.copyOf(buffer, buffer.length);
+        } else {
+            return null;
+        }
+    }
+
+    public ByteBuffer toByteBuffer() {
+        if (buffer != null) {
+            return ByteBuffer.wrap(toArray());
+        } else {
+            return null;
+        }
+    }
+
+    public byte getByteAt(int i) {
+        return buffer[i];
+    }
+
+    public boolean hasValidPrefix() {
+        return startsWith(buffer, PREFIX);
+    }
+
+    public boolean isSaslHeader() {
+        return getProtocolId() == SASL_PROTOCOL_ID;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((buffer == null) ? 0 : Arrays.hashCode(buffer));
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        AMQPHeader other = (AMQPHeader) obj;
+        if (buffer == null) {
+            if (other.buffer != null) {
+                return false;
+            }
+        }
+
+        return Arrays.equals(buffer, other.buffer);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < buffer.length; ++i) {
+            char value = (char) buffer[i];
+            if (Character.isLetter(value)) {
+                builder.append(value);
+            } else {
+                builder.append(",");
+                builder.append((int) value);
+            }
+        }
+        return builder.toString();
+    }
+
+    private boolean startsWith(byte[] buffer, byte[] value) {
+        if (buffer == null || buffer.length < value.length) {
+            return false;
+        }
+
+        for (int i = 0; i < value.length; ++i) {
+            if (buffer[i] != value[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private AMQPHeader setBuffer(byte[] buffer, boolean validate) {
+        if (validate) {
+            if (buffer.length != 8 || !startsWith(buffer, PREFIX)) {
+                throw new IllegalArgumentException("Not an AMQP header buffer");
+            }
+
+            validateProtocolByte(buffer[PROTOCOL_ID_INDEX]);
+            validateMajorVersionByte(buffer[MAJOR_VERSION_INDEX]);
+            validateMinorVersionByte(buffer[MINOR_VERSION_INDEX]);
+            validateRevisionByte(buffer[REVISION_INDEX]);
+        }
+
+        if (buffer.length > HEADER_SIZE_BYTES) {
+            throw new IndexOutOfBoundsException("Buffer is to large to be an AMQP Header value");
+        }
+
+        this.buffer = buffer;
+        return this;
+    }
+
+    /**
+     * Called to validate a byte according to a given index within the AMQP Header
+     *
+     * If the index is outside the range of the header size an {@link IndexOutOfBoundsException}
+     * will be thrown.
+     *
+     * @param index
+     *      The index in the header where the byte should be validated.
+     * @param value
+     *      The value to check validity of in the given index in the AMQP Header.
+     *
+     * @throws IllegalArgumentException if the value is not valid for the index given in the AMQP header
+     * @throws IndexOutOfBoundsException if the index value is greater than the AMQP header size.
+     */
+    public static void validateByte(int index, byte value) {
+        switch (index) {
+            case 0:
+                validatePrefixByte1(value);
+                break;
+            case 1:
+                validatePrefixByte2(value);
+                break;
+            case 2:
+                validatePrefixByte3(value);
+                break;
+            case 3:
+                validatePrefixByte4(value);
+                break;
+            case 4:
+                validateProtocolByte(value);
+                break;
+            case 5:
+                validateMajorVersionByte(value);
+                break;
+            case 6:
+                validateMinorVersionByte(value);
+                break;
+            case 7:
+                validateRevisionByte(value);
+                break;
+            default:
+                throw new IndexOutOfBoundsException("Invalid AMQP Header byte index provided to validation method: " + index);
+        }
+    }
+
+    private static void validatePrefixByte1(byte value) {
+        if (value != PREFIX[0]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(1) specified %d : expected %d", value, PREFIX[0]));
+        }
+    }
+
+    private static void validatePrefixByte2(byte value) {
+        if (value != PREFIX[1]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(2) specified %d : expected %d", value, PREFIX[1]));
+        }
+    }
+
+    private static void validatePrefixByte3(byte value) {
+        if (value != PREFIX[2]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(3) specified %d : expected %d", value, PREFIX[2]));
+        }
+    }
+
+    private static void validatePrefixByte4(byte value) {
+        if (value != PREFIX[3]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(4) specified %d : expected %d", value, PREFIX[3]));
+        }
+    }
+
+    private static void validateProtocolByte(byte value) {
+        if (value != AMQP_PROTOCOL_ID && value != SASL_PROTOCOL_ID) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid protocol Id specified %d : expected one of %d or %d",
+                value, AMQP_PROTOCOL_ID, SASL_PROTOCOL_ID));
+        }
+    }
+
+    private static void validateMajorVersionByte(byte value) {
+        if (value != 1) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid Major version specified %d : expected %d", value, 1));
+        }
+    }
+
+    private static void validateMinorVersionByte(byte value) {
+        if (value != 0) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid Minor version specified %d : expected %d", value, 0));
+        }
+    }
+
+    private static void validateRevisionByte(byte value) {
+        if (value != 0) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid revision specified %d : expected %d", value, 0));
+        }
+    }
+
+    /**
+     * Provide this AMQP Header with a handler that will process the given AMQP header
+     * depending on the protocol type the correct handler method is invoked.
+     *
+     * @param handler
+     *      The {@link HeaderHandler} instance to use to process the header.
+     * @param context
+     *      A context object to pass along with the header.
+     *
+     * @param <E> The type that will be passed as the context for this event
+     */
+    public <E> void invoke(HeaderHandler<E> handler, E context) {
+        if (isSaslHeader()) {
+            handler.handleSASLHeader(this, context);
+        } else {
+            handler.handleAMQPHeader(this, context);
+        }
+    }
+
+    public interface HeaderHandler<E> {
+
+        default void handleAMQPHeader(AMQPHeader header, E context) {}
+
+        default void handleSASLHeader(AMQPHeader header, E context) {}
+
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Attach.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Attach.java
new file mode 100644
index 0000000..45a18aa
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Attach.java
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+
+import io.netty.buffer.ByteBuf;
+
+public class Attach extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:attach:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000012L);
+
+    /**
+     * Enumeration which maps to fields in the Attach Performative
+     */
+    public enum Field {
+        NAME,
+        HANDLE,
+        ROLE,
+        SND_SETTLE_MODE,
+        RCV_SETTLE_MODE,
+        SOURCE,
+        TARGET,
+        UNSETTLED,
+        INCOMPLETE_UNSETTLED,
+        INITIAL_DELIVERY_COUNT,
+        MAX_MESSAGE_SIZE,
+        OFFERED_CAPABILITIES,
+        DESIRED_CAPABILITIES,
+        PROPERTIES
+    }
+
+    public Attach() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Attach(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Attach(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Attach setName(String o) {
+        getList().set(Field.NAME.ordinal(), o);
+        return this;
+    }
+
+    public String getName() {
+        return (String) getList().get(Field.NAME.ordinal());
+    }
+
+    public Attach setHandle(UnsignedInteger o) {
+        getList().set(Field.HANDLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getHandle() {
+        return (UnsignedInteger) getList().get(Field.HANDLE.ordinal());
+    }
+
+    public Attach setRole(Boolean o) {
+        getList().set(Field.ROLE.ordinal(), o);
+        return this;
+    }
+
+    public Attach setRole(boolean o) {
+        getList().set(Field.ROLE.ordinal(), o);
+        return this;
+    }
+
+    public Attach setRole(Role role) {
+        getList().set(Field.ROLE.ordinal(), role.getValue());
+        return this;
+    }
+
+    public Boolean getRole() {
+        return (Boolean) getList().get(Field.ROLE.ordinal());
+    }
+
+    public Attach setSenderSettleMode(byte o) {
+        getList().set(Field.SND_SETTLE_MODE.ordinal(), UnsignedByte.valueOf(o));
+        return this;
+    }
+
+    public Attach setSenderSettleMode(UnsignedByte o) {
+        getList().set(Field.SND_SETTLE_MODE.ordinal(), o);
+        return this;
+    }
+
+    public Attach setSenderSettleMode(SenderSettleMode o) {
+        getList().set(Field.SND_SETTLE_MODE.ordinal(), o.getValue());
+        return this;
+    }
+
+    public UnsignedByte getSenderSettleMode() {
+        return (UnsignedByte) getList().get(Field.SND_SETTLE_MODE.ordinal());
+    }
+
+    public Attach setReceiverSettleMode(byte o) {
+        getList().set(Field.RCV_SETTLE_MODE.ordinal(), UnsignedByte.valueOf(o));
+        return this;
+    }
+
+    public Attach setReceiverSettleMode(UnsignedByte o) {
+        getList().set(Field.RCV_SETTLE_MODE.ordinal(), o);
+        return this;
+    }
+
+    public Attach setReceiverSettleMode(ReceiverSettleMode o) {
+        getList().set(Field.RCV_SETTLE_MODE.ordinal(), o.getValue());
+        return this;
+    }
+
+    public UnsignedByte getReceiverSettleMode() {
+        return (UnsignedByte) getList().get(Field.RCV_SETTLE_MODE.ordinal());
+    }
+
+    public Attach setSource(Source o) {
+        getList().set(Field.SOURCE.ordinal(), o);
+        return this;
+    }
+
+    public Source getSource() {
+        return (Source) getList().get(Field.SOURCE.ordinal());
+    }
+
+    public Attach setTarget(Target o) {
+        getList().set(Field.TARGET.ordinal(), o);
+        return this;
+    }
+
+    public Attach setTarget(Coordinator o) {
+        getList().set(Field.TARGET.ordinal(), o);
+        return this;
+    }
+
+    public Object getTarget() {
+        return getList().get(Field.TARGET.ordinal());
+    }
+
+    public Attach setUnsettled(Map<Binary, DescribedType> o) {
+        getList().set(Field.UNSETTLED.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Binary, DescribedType> getUnsettled() {
+        return (Map<Binary, DescribedType>) getList().get(Field.UNSETTLED.ordinal());
+    }
+
+    public Attach setIncompleteUnsettled(Boolean o) {
+        getList().set(Field.INCOMPLETE_UNSETTLED.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getIncompleteUnsettled() {
+        return (Boolean) getList().get(Field.INCOMPLETE_UNSETTLED.ordinal());
+    }
+
+    public Attach setInitialDeliveryCount(UnsignedInteger o) {
+        getList().set(Field.INITIAL_DELIVERY_COUNT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getInitialDeliveryCount() {
+        return (UnsignedInteger) getList().get(Field.INITIAL_DELIVERY_COUNT.ordinal());
+    }
+
+    public Attach setMaxMessageSize(UnsignedLong o) {
+        getList().set(Field.MAX_MESSAGE_SIZE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedLong getMaxMessageSize() {
+        return (UnsignedLong) getList().get(Field.MAX_MESSAGE_SIZE.ordinal());
+    }
+
+    public Attach setOfferedCapabilities(Symbol[] o) {
+        getList().set(Field.OFFERED_CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getOfferedCapabilities() {
+        return (Symbol[]) getList().get(Field.OFFERED_CAPABILITIES.ordinal());
+    }
+
+    public Attach setDesiredCapabilities(Symbol[] o) {
+        getList().set(Field.DESIRED_CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getDesiredCapabilities() {
+        return (Symbol[]) getList().get(Field.DESIRED_CAPABILITIES.ordinal());
+    }
+
+    public Attach setProperties(Map<Symbol, Object> o) {
+        getList().set(Field.PROPERTIES.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getProperties() {
+        return (Map<Symbol, Object>) getList().get(Field.PROPERTIES.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.ATTACH;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleAttach(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case SND_SETTLE_MODE:
+                    result = SenderSettleMode.MIXED;
+                    break;
+                case RCV_SETTLE_MODE:
+                    result = ReceiverSettleMode.FIRST;
+                    break;
+                case INCOMPLETE_UNSETTLED:
+                    result = Boolean.FALSE;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Begin.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Begin.java
new file mode 100644
index 0000000..5bdd311
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Begin.java
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+public class Begin extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:begin:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000011L);
+
+    /**
+     * Enumeration which maps to fields in the Begin Performative
+     */
+    public enum Field {
+        REMOTE_CHANNEL,
+        NEXT_OUTGOING_ID,
+        INCOMING_WINDOW,
+        OUTGOING_WINDOW,
+        HANDLE_MAX,
+        OFFERED_CAPABILITIES,
+        DESIRED_CAPABILITIES,
+        PROPERTIES,
+    }
+
+    public Begin() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Begin(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Begin(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Begin setRemoteChannel(UnsignedShort o) {
+        getList().set(Field.REMOTE_CHANNEL.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedShort getRemoteChannel() {
+        return (UnsignedShort) getList().get(Field.REMOTE_CHANNEL.ordinal());
+    }
+
+    public Begin setNextOutgoingId(UnsignedInteger o) {
+        getList().set(Field.NEXT_OUTGOING_ID.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getNextOutgoingId() {
+        return (UnsignedInteger) getList().get(Field.NEXT_OUTGOING_ID.ordinal());
+    }
+
+    public Begin setIncomingWindow(UnsignedInteger o) {
+        getList().set(Field.INCOMING_WINDOW.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getIncomingWindow() {
+        return (UnsignedInteger) getList().get(Field.INCOMING_WINDOW.ordinal());
+    }
+
+    public Begin setOutgoingWindow(UnsignedInteger o) {
+        getList().set(Field.OUTGOING_WINDOW.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getOutgoingWindow() {
+        return (UnsignedInteger) getList().get(Field.OUTGOING_WINDOW.ordinal());
+    }
+
+    public Begin setHandleMax(UnsignedInteger o) {
+        getList().set(Field.HANDLE_MAX.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getHandleMax() {
+        return (UnsignedInteger) getList().get(Field.HANDLE_MAX.ordinal());
+    }
+
+    public Begin setOfferedCapabilities(Symbol[] o) {
+        getList().set(Field.OFFERED_CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getOfferedCapabilities() {
+        return (Symbol[]) getList().get(Field.OFFERED_CAPABILITIES.ordinal());
+    }
+
+    public Begin setDesiredCapabilities(Symbol[] o) {
+        getList().set(Field.DESIRED_CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getDesiredCapabilities() {
+        return (Symbol[]) getList().get(Field.DESIRED_CAPABILITIES.ordinal());
+    }
+
+    public Begin setProperties(Map<Symbol, Object> o) {
+        getList().set(Field.PROPERTIES.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getProperties() {
+        return (Map<Symbol, Object>) getList().get(Field.PROPERTIES.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.BEGIN;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleBegin(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case HANDLE_MAX:
+                    result = UnsignedInteger.MAX_VALUE;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Close.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Close.java
new file mode 100644
index 0000000..306f1a1
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Close.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+public class Close extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:close:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000018L);
+
+    /**
+     * Enumeration which maps to fields in the Close Performative
+     */
+    public enum Field {
+        ERROR
+    }
+
+    public Close() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Close(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Close(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Close setError(ErrorCondition o) {
+        getList().set(Field.ERROR.ordinal(), o);
+        return this;
+    }
+
+    public ErrorCondition getError() {
+        return (ErrorCondition) getList().get(Field.ERROR.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.CLOSE;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleClose(this, payload, channel, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/DeliveryState.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/DeliveryState.java
new file mode 100644
index 0000000..d777807
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/DeliveryState.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+
+/**
+ * Describes the state of a delivery at a link end-point.
+ *
+ * Note that the the sender is the owner of the state.
+ * The receiver merely influences the state.
+ */
+public interface DeliveryState extends DescribedType {
+
+    enum DeliveryStateType {
+        Accepted,
+        Declared,
+        Modified,
+        Received,
+        Rejected,
+        Released,
+        Transactional
+    }
+
+    /**
+     * @return the {@link DeliveryStateType} that this instance represents.
+     */
+    DeliveryStateType getType();
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Detach.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Detach.java
new file mode 100644
index 0000000..67dcc7d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Detach.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+public class Detach extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:detach:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000016L);
+
+    /**
+     * Enumeration which maps to fields in the Detach Performative
+     */
+    public enum Field {
+        HANDLE,
+        CLOSED,
+        ERROR
+    }
+
+    public Detach() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Detach(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Detach(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public UnsignedLong getDescriptor() {
+        return DESCRIPTOR_CODE;
+    }
+
+    public Detach setHandle(UnsignedInteger o) {
+        getList().set(Field.HANDLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getHandle() {
+        return (UnsignedInteger) getList().get(Field.HANDLE.ordinal());
+    }
+
+    public Detach setClosed(Boolean o) {
+        getList().set(Field.CLOSED.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getClosed() {
+        return (Boolean) getList().get(Field.CLOSED.ordinal());
+    }
+
+    public Detach setError(ErrorCondition o) {
+        getList().set(Field.ERROR.ordinal(), o);
+        return this;
+    }
+
+    public Object getError() {
+        return getList().get(Field.ERROR.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.DETACH;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleDetach(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case CLOSED:
+                    result = Boolean.FALSE;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Disposition.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Disposition.java
new file mode 100644
index 0000000..c533067
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Disposition.java
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+public class Disposition extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:disposition:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000015L);
+
+    /**
+     * Enumeration which maps to fields in the Disposition Performative
+     */
+    public enum Field {
+        ROLE,
+        FIRST,
+        LAST,
+        SETTLED,
+        STATE,
+        BATCHABLE
+    }
+
+    public Disposition() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Disposition(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Disposition(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Disposition setRole(Boolean o) {
+        getList().set(Field.ROLE.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getRole() {
+        return (Boolean) getList().get(Field.ROLE.ordinal());
+    }
+
+    public Disposition setFirst(UnsignedInteger o) {
+        getList().set(Field.FIRST.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getFirst() {
+        return (UnsignedInteger) getList().get(Field.FIRST.ordinal());
+    }
+
+    public Disposition setLast(UnsignedInteger o) {
+        getList().set(Field.LAST.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getLast() {
+        return (UnsignedInteger) getList().get(Field.LAST.ordinal());
+    }
+
+    public Disposition setSettled(Boolean o) {
+        getList().set(Field.SETTLED.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getSettled() {
+        return (Boolean) getList().get(Field.SETTLED.ordinal());
+    }
+
+    public Disposition setState(DescribedType o) {
+        getList().set(Field.STATE.ordinal(), o);
+        return this;
+    }
+
+    public DescribedType getState() {
+        return (DescribedType) getList().get(Field.STATE.ordinal());
+    }
+
+    public Disposition setBatchable(Boolean o) {
+        getList().set(Field.BATCHABLE.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getBatchable() {
+        return (Boolean) getList().get(Field.BATCHABLE.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.DISPOSITION;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleDisposition(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case SETTLED:
+                    result = Boolean.FALSE;
+                    break;
+                case BATCHABLE:
+                    result = Boolean.FALSE;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/End.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/End.java
new file mode 100644
index 0000000..42a8290
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/End.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+public class End extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:end:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000017L);
+
+    /**
+     * Enumeration which maps to fields in the End Performative
+     */
+    public enum Field {
+        ERROR
+    }
+
+    public End() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public End(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public End(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public End setError(ErrorCondition o) {
+        getList().set(Field.ERROR.ordinal(), o);
+        return this;
+    }
+
+    public ErrorCondition getError() {
+        return (ErrorCondition) getList().get(Field.ERROR.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.END;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleEnd(this, payload, channel, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/ErrorCondition.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/ErrorCondition.java
new file mode 100644
index 0000000..04b9817
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/ErrorCondition.java
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+public class ErrorCondition extends ListDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:error:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000001dL);
+
+    /**
+     * Enumeration which maps to fields in the ErrorCondition Performative
+     */
+    public enum Field {
+        CONDITION,
+        DESCRIPTION,
+        INFO
+    }
+
+    public ErrorCondition() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public ErrorCondition(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public ErrorCondition(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    public ErrorCondition(Symbol condition, String description) {
+        super(Field.values().length);
+
+        setCondition(condition);
+        setDescription(description);
+    }
+
+    public ErrorCondition(String condition, String description) {
+        super(Field.values().length);
+
+        setCondition(Symbol.valueOf(condition));
+        setDescription(description);
+    }
+
+    public ErrorCondition(Symbol condition, String description, Map<Symbol, Object> info) {
+        super(Field.values().length);
+
+        setCondition(condition);
+        setDescription(description);
+        setInfo(info);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public ErrorCondition setCondition(Symbol o) {
+        getList().set(Field.CONDITION.ordinal(), o);
+        return this;
+    }
+
+    public Symbol getCondition() {
+        return (Symbol) getList().get(Field.CONDITION.ordinal());
+    }
+
+    public ErrorCondition setDescription(String o) {
+        getList().set(Field.DESCRIPTION.ordinal(), o);
+        return this;
+    }
+
+    public String getDescription() {
+        return (String) getList().get(Field.DESCRIPTION.ordinal());
+    }
+
+    public ErrorCondition setInfo(Map<Symbol, Object> o) {
+        getList().set(Field.INFO.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getInfo() {
+        return (Map<Symbol, Object>) getList().get(Field.INFO.ordinal());
+    }
+
+    @Override
+    public int hashCode() {
+        return System.identityHashCode(this);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof DescribedType)) {
+            return false;
+        }
+
+        DescribedType d = (DescribedType) obj;
+        if (!(DESCRIPTOR_CODE.equals(d.getDescriptor()) || DESCRIPTOR_SYMBOL.equals(d.getDescriptor()))) {
+            return false;
+        }
+
+        Object described = getDescribed();
+        Object described2 = d.getDescribed();
+        if (described == null) {
+            return described2 == null;
+        } else {
+            return described.equals(described2);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Flow.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Flow.java
new file mode 100644
index 0000000..a164f75
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Flow.java
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+public class Flow extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:flow:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000013L);
+
+    /**
+     * Enumeration which maps to fields in the Flow Performative
+     */
+    public enum Field {
+        NEXT_INCOMING_ID,
+        INCOMING_WINDOW,
+        NEXT_OUTGOING_ID,
+        OUTGOING_WINDOW,
+        HANDLE,
+        DELIVERY_COUNT,
+        LINK_CREDIT,
+        AVAILABLE,
+        DRAIN,
+        ECHO,
+        PROPERTIES,
+    }
+
+    public Flow() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Flow(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Flow(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Flow setNextIncomingId(UnsignedInteger o) {
+        getList().set(Field.NEXT_INCOMING_ID.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getNextIncomingId() {
+        return (UnsignedInteger) getList().get(Field.NEXT_INCOMING_ID.ordinal());
+    }
+
+    public Flow setIncomingWindow(UnsignedInteger o) {
+        getList().set(Field.INCOMING_WINDOW.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getIncomingWindow() {
+        return (UnsignedInteger) getList().get(Field.INCOMING_WINDOW.ordinal());
+    }
+
+    public Flow setNextOutgoingId(UnsignedInteger o) {
+        getList().set(Field.NEXT_OUTGOING_ID.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getNextOutgoingId() {
+        return (UnsignedInteger) getList().get(Field.NEXT_OUTGOING_ID.ordinal());
+    }
+
+    public Flow setOutgoingWindow(UnsignedInteger o) {
+        getList().set(Field.OUTGOING_WINDOW.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getOutgoingWindow() {
+        return (UnsignedInteger) getList().get(Field.OUTGOING_WINDOW.ordinal());
+    }
+
+    public Flow setHandle(UnsignedInteger o) {
+        getList().set(Field.HANDLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getHandle() {
+        return (UnsignedInteger) getList().get(Field.HANDLE.ordinal());
+    }
+
+    public Flow setDeliveryCount(UnsignedInteger o) {
+        getList().set(Field.DELIVERY_COUNT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getDeliveryCount() {
+        return (UnsignedInteger) getList().get(Field.DELIVERY_COUNT.ordinal());
+    }
+
+    public Flow setLinkCredit(UnsignedInteger o) {
+        getList().set(Field.LINK_CREDIT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getLinkCredit() {
+        return (UnsignedInteger) getList().get(Field.LINK_CREDIT.ordinal());
+    }
+
+    public Flow setAvailable(UnsignedInteger o) {
+        getList().set(Field.AVAILABLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getAvailable() {
+        return (UnsignedInteger) getList().get(Field.AVAILABLE.ordinal());
+    }
+
+    public Flow setDrain(Boolean o) {
+        getList().set(Field.DRAIN.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getDrain() {
+        return (Boolean) getList().get(Field.DRAIN.ordinal());
+    }
+
+    public Flow setEcho(Boolean o) {
+        getList().set(Field.ECHO.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getEcho() {
+        return (Boolean) getList().get(Field.ECHO.ordinal());
+    }
+
+    public Flow setProperties(Map<Symbol, Object> o) {
+        getList().set(Field.PROPERTIES.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getProperties() {
+        return (Map<Symbol, Object>) getList().get(Field.PROPERTIES.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.FLOW;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleFlow(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case DRAIN:
+                    result = Boolean.FALSE;
+                    break;
+                case ECHO:
+                    result = Boolean.FALSE;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/HeartBeat.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/HeartBeat.java
new file mode 100644
index 0000000..4dcf03e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/HeartBeat.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Dummy Performative that is fired whenever an Empty frame is received
+ */
+public class HeartBeat extends PerformativeDescribedType {
+
+    public static final HeartBeat INSTANCE = new HeartBeat();
+
+    private HeartBeat() {
+        super(0);
+    }
+
+    @Override
+    public Object getDescriptor() {
+        return null;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.HEARTBEAT;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleHeartBeat(INSTANCE, payload, channel, context);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Open.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Open.java
new file mode 100644
index 0000000..dd32085
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Open.java
@@ -0,0 +1,187 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+
+import io.netty.buffer.ByteBuf;
+
+public class Open extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:open:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000010L);
+
+    /**
+     * Enumeration which maps to fields in the Open Performative
+     */
+    public enum Field {
+        CONTAINER_ID,
+        HOSTNAME,
+        MAX_FRAME_SIZE,
+        CHANNEL_MAX,
+        IDLE_TIME_OUT,
+        OUTGOING_LOCALES,
+        INCOMING_LOCALES,
+        OFFERED_CAPABILITIES,
+        DESIRED_CAPABILITIES,
+        PROPERTIES,
+    }
+
+    public Open() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Open(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Open(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Open setContainerId(String o) {
+        getList().set(Field.CONTAINER_ID.ordinal(), o);
+        return this;
+    }
+
+    public String getContainerId() {
+        return (String) getList().get(Field.CONTAINER_ID.ordinal());
+    }
+
+    public Open setHostname(String o) {
+        getList().set(Field.HOSTNAME.ordinal(), o);
+        return this;
+    }
+
+    public String getHostname() {
+        return (String) getList().get(Field.HOSTNAME.ordinal());
+    }
+
+    public Open setMaxFrameSize(UnsignedInteger o) {
+        getList().set(Field.MAX_FRAME_SIZE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getMaxFrameSize() {
+        return (UnsignedInteger) getList().get(Field.MAX_FRAME_SIZE.ordinal());
+    }
+
+    public Open setChannelMax(UnsignedShort o) {
+        getList().set(Field.CHANNEL_MAX.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedShort getChannelMax() {
+        return (UnsignedShort) getList().get(Field.CHANNEL_MAX.ordinal());
+    }
+
+    public Open setIdleTimeOut(UnsignedInteger o) {
+        getList().set(Field.IDLE_TIME_OUT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getIdleTimeOut() {
+        return (UnsignedInteger) getList().get(Field.IDLE_TIME_OUT.ordinal());
+    }
+
+    public Open setOutgoingLocales(Symbol[] o) {
+        getList().set(Field.OUTGOING_LOCALES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getOutgoingLocales() {
+        return (Symbol[]) getList().get(Field.OUTGOING_LOCALES.ordinal());
+    }
+
+    public Open setIncomingLocales(Symbol[] o) {
+        getList().set(Field.INCOMING_LOCALES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getIncomingLocales() {
+        return (Symbol[]) getList().get(Field.INCOMING_LOCALES.ordinal());
+    }
+
+    public Open setOfferedCapabilities(Symbol[] o) {
+        getList().set(Field.OFFERED_CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getOfferedCapabilities() {
+        return (Symbol[]) getList().get(Field.OFFERED_CAPABILITIES.ordinal());
+    }
+
+    public Open setDesiredCapabilities(Symbol[] o) {
+        getList().set(Field.DESIRED_CAPABILITIES.ordinal(), o);
+        return this;
+    }
+
+    public Symbol[] getDesiredCapabilities() {
+        return (Symbol[]) getList().get(Field.DESIRED_CAPABILITIES.ordinal());
+    }
+
+    public Open setProperties(Map<Symbol, Object> o) {
+        getList().set(Field.PROPERTIES.ordinal(), o);
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<Symbol, Object> getProperties() {
+        return (Map<Symbol, Object>) getList().get(Field.PROPERTIES.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.OPEN;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleOpen(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case MAX_FRAME_SIZE:
+                    result = UnsignedInteger.MAX_VALUE;
+                    break;
+                case CHANNEL_MAX:
+                    result = UnsignedShort.MAX_VALUE;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/PerformativeDescribedType.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/PerformativeDescribedType.java
new file mode 100644
index 0000000..fd61af1
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/PerformativeDescribedType.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * AMQP Performative marker class for DescribedType elements in this codec.
+ */
+public abstract class PerformativeDescribedType extends ListDescribedType {
+
+    public enum PerformativeType {
+        ATTACH,
+        BEGIN,
+        CLOSE,
+        DETACH,
+        DISPOSITION,
+        END,
+        FLOW,
+        OPEN,
+        TRANSFER,
+        HEARTBEAT
+    }
+
+    public PerformativeDescribedType(int numberOfFields) {
+        super(numberOfFields);
+    }
+
+    public PerformativeDescribedType(int numberOfFields, List<Object> described) {
+        super(numberOfFields, described);
+    }
+
+    public abstract PerformativeType getPerformativeType();
+
+    public interface PerformativeHandler<E> {
+
+        default void handleOpen(Open open, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Open was not handled");
+        }
+        default void handleBegin(Begin begin, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Begin was not handled");
+        }
+        default void handleAttach(Attach attach, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Attach was not handled");
+        }
+        default void handleFlow(Flow flow, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Flow was not handled");
+        }
+        default void handleTransfer(Transfer transfer, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Transfer was not handled");
+        }
+        default void handleDisposition(Disposition disposition, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Disposition was not handled");
+        }
+        default void handleDetach(Detach detach, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Detach was not handled");
+        }
+        default void handleEnd(End end, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP End was not handled");
+        }
+        default void handleClose(Close close, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Close was not handled");
+        }
+        default void handleHeartBeat(HeartBeat thump, ByteBuf payload, int channel, E context) {
+            throw new AssertionError("AMQP Heart Beat frame was not handled");
+        }
+    }
+
+    public Object getFieldValueOrSpecDefault(int index) {
+        return getFieldValue(index);
+    }
+
+    public abstract <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context);
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/ReceiverSettleMode.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/ReceiverSettleMode.java
new file mode 100644
index 0000000..7c25a7b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/ReceiverSettleMode.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+
+public enum ReceiverSettleMode {
+
+    FIRST(0), SECOND(1);
+
+    private UnsignedByte value;
+
+    private ReceiverSettleMode(int value) {
+        this.value = UnsignedByte.valueOf((byte)value);
+    }
+
+    public static ReceiverSettleMode valueOf(UnsignedByte value) {
+        return value == null ? FIRST : ReceiverSettleMode.valueOf(value.byteValue());
+    }
+
+    public static ReceiverSettleMode valueOf(byte value) {
+        switch (value) {
+            case 0:
+                return ReceiverSettleMode.FIRST;
+            case 1:
+                return ReceiverSettleMode.SECOND;
+            default:
+                throw new IllegalArgumentException("The value can be only 0 (for FIRST) and 1 (for SECOND)");
+        }
+    }
+
+    public byte byteValue() {
+        return value.byteValue();
+    }
+
+    public UnsignedByte getValue() {
+        return value;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Role.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Role.java
new file mode 100644
index 0000000..dcef294
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Role.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+public enum Role {
+
+    SENDER(false), RECEIVER(true);
+
+    private final boolean receiver;
+
+    private Role(boolean receiver) {
+        this.receiver = receiver;
+    }
+
+    public boolean getValue() {
+        return receiver;
+    }
+
+    public static Role valueOf(boolean role) {
+        if (role) {
+            return RECEIVER;
+        } else {
+            return SENDER;
+        }
+    }
+
+    public static Role valueOf(Boolean role) {
+        if (Boolean.TRUE.equals(role)) {
+            return RECEIVER;
+        } else {
+            return SENDER;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/SenderSettleMode.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/SenderSettleMode.java
new file mode 100644
index 0000000..648717f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/SenderSettleMode.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+
+public enum SenderSettleMode {
+
+    UNSETTLED(0), SETTLED(1), MIXED(2);
+
+    private UnsignedByte value;
+
+    private SenderSettleMode(int value) {
+        this.value = UnsignedByte.valueOf((byte)value);
+    }
+
+    public static SenderSettleMode valueOf(UnsignedByte value) {
+        return value == null ? MIXED : SenderSettleMode.valueOf(value.byteValue());
+    }
+
+    public static SenderSettleMode valueOf(byte value) {
+        switch (value) {
+            case 0:
+                return SenderSettleMode.UNSETTLED;
+            case 1:
+                return SenderSettleMode.SETTLED;
+            case 2:
+                return SenderSettleMode.MIXED;
+            default:
+                throw new IllegalArgumentException("The value can be only 0 (for UNSETTLED), 1 (for SETTLED) and 2 (for MIXED)");
+        }
+    }
+
+    public byte byteValue() {
+        return value.byteValue();
+    }
+
+    public UnsignedByte getValue() {
+        return value;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Transfer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Transfer.java
new file mode 100644
index 0000000..9600c0b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/transport/Transfer.java
@@ -0,0 +1,203 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+
+import io.netty.buffer.ByteBuf;
+
+public class Transfer extends PerformativeDescribedType {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:transfer:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000014L);
+
+    /**
+     * Enumeration which maps to fields in the Transfer Performative
+     */
+    public enum Field {
+        HANDLE,
+        DELIVERY_ID,
+        DELIVERY_TAG,
+        MESSAGE_FORMAT,
+        SETTLED,
+        MORE,
+        RCV_SETTLE_MODE,
+        STATE,
+        RESUME,
+        ABORTED,
+        BATCHABLE
+    }
+
+    public Transfer() {
+        super(Field.values().length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Transfer(Object described) {
+        super(Field.values().length, (List<Object>) described);
+    }
+
+    public Transfer(List<Object> described) {
+        super(Field.values().length, described);
+    }
+
+    @Override
+    public Symbol getDescriptor() {
+        return DESCRIPTOR_SYMBOL;
+    }
+
+    public Transfer setHandle(UnsignedInteger o) {
+        getList().set(Field.HANDLE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getHandle() {
+        return (UnsignedInteger) getList().get(Field.HANDLE.ordinal());
+    }
+
+    public Transfer setDeliveryId(UnsignedInteger o) {
+        getList().set(Field.DELIVERY_ID.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getDeliveryId() {
+        return (UnsignedInteger) getList().get(Field.DELIVERY_ID.ordinal());
+    }
+
+    public Transfer setDeliveryTag(Binary o) {
+        getList().set(Field.DELIVERY_TAG.ordinal(), o);
+        return this;
+    }
+
+    public Binary getDeliveryTag() {
+        return (Binary) getList().get(Field.DELIVERY_TAG.ordinal());
+    }
+
+    public Transfer setMessageFormat(UnsignedInteger o) {
+        getList().set(Field.MESSAGE_FORMAT.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedInteger getMessageFormat() {
+        return (UnsignedInteger) getList().get(Field.MESSAGE_FORMAT.ordinal());
+    }
+
+    public Transfer setSettled(Boolean o) {
+        getList().set(Field.SETTLED.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getSettled() {
+        return (Boolean) getList().get(Field.SETTLED.ordinal());
+    }
+
+    public Transfer setMore(Boolean o) {
+        getList().set(Field.MORE.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getMore() {
+        return (Boolean) getList().get(Field.MORE.ordinal());
+    }
+
+    public Transfer setRcvSettleMode(UnsignedByte o) {
+        getList().set(Field.RCV_SETTLE_MODE.ordinal(), o);
+        return this;
+    }
+
+    public UnsignedByte getRcvSettleMode() {
+        return (UnsignedByte) getList().get(Field.RCV_SETTLE_MODE.ordinal());
+    }
+
+    public Transfer setState(DescribedType o) {
+        getList().set(Field.STATE.ordinal(), o);
+        return this;
+    }
+
+    public DescribedType getState() {
+        return (DescribedType) getList().get(Field.STATE.ordinal());
+    }
+
+    public Transfer setResume(Boolean o) {
+        getList().set(Field.RESUME.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getResume() {
+        return (Boolean) getList().get(Field.RESUME.ordinal());
+    }
+
+    public Transfer setAborted(Boolean o) {
+        getList().set(Field.ABORTED.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getAborted() {
+        return (Boolean) getList().get(Field.ABORTED.ordinal());
+    }
+
+    public Transfer setBatchable(Boolean o) {
+        getList().set(Field.BATCHABLE.ordinal(), o);
+        return this;
+    }
+
+    public Boolean getBatchable() {
+        return (Boolean) getList().get(Field.BATCHABLE.ordinal());
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.TRANSFER;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ByteBuf payload, int channel, E context) {
+        handler.handleTransfer(this, payload, channel, context);
+    }
+
+    @Override
+    public Object getFieldValueOrSpecDefault(int index) {
+        Object result = getFieldValue(index);
+        if (result == null) {
+            Field field = Field.values()[index];
+            switch (field) {
+                case MORE:
+                    result = Boolean.FALSE;
+                    break;
+                case RESUME:
+                    result = Boolean.FALSE;
+                    break;
+                case ABORTED:
+                    result = Boolean.FALSE;
+                    break;
+                case BATCHABLE:
+                    result = Boolean.FALSE;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/util/TypeMapper.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/util/TypeMapper.java
new file mode 100644
index 0000000..4d6cfaa
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/codec/util/TypeMapper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+
+public abstract class TypeMapper {
+
+    private TypeMapper() {
+    }
+
+    public static Symbol[] toSymbolArray(String[] stringArray) {
+        Symbol[] result = null;
+
+        if (stringArray != null) {
+            result = new Symbol[stringArray.length];
+            for (int i = 0; i < stringArray.length; ++i) {
+                result[i] = Symbol.valueOf(stringArray[i]);
+            }
+        }
+
+        return result;
+    }
+
+    public static Map<Symbol, Object> toSymbolKeyedMap(Map<String, Object> stringsMap) {
+        final Map<Symbol, Object> result;
+
+        if (stringsMap != null) {
+            result = new HashMap<>(stringsMap.size());
+            stringsMap.forEach((key, value) -> {
+                result.put(Symbol.valueOf(key), value);
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/exceptions/UnexpectedPerformativeError.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/exceptions/UnexpectedPerformativeError.java
new file mode 100644
index 0000000..5a28be8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/exceptions/UnexpectedPerformativeError.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.exceptions;
+
+public class UnexpectedPerformativeError extends AssertionError {
+
+    private static final long serialVersionUID = -935990491796615868L;
+
+    public UnexpectedPerformativeError() {
+    }
+
+    public UnexpectedPerformativeError(Object detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(boolean detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(char detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(int detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(long detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(float detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(double detailMessage) {
+        super(detailMessage);
+    }
+
+    public UnexpectedPerformativeError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AMQPHeaderExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AMQPHeaderExpectation.java
new file mode 100644
index 0000000..3251600
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AMQPHeaderExpectation.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedExpectation;
+import org.apache.qpid.protonj2.test.driver.actions.AMQPHeaderInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.ByteBufferInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Expectation entry for AMQP Headers
+ */
+public class AMQPHeaderExpectation implements ScriptedExpectation {
+
+    private final AMQPHeader expected;
+    private final AMQPTestDriver driver;
+
+    public AMQPHeaderExpectation(AMQPHeader expected, AMQPTestDriver driver) {
+        this.expected = expected;
+        this.driver = driver;
+    }
+
+    public AMQPHeaderInjectAction respondWithAMQPHeader() {
+        AMQPHeaderInjectAction response = new AMQPHeaderInjectAction(driver, AMQPHeader.getAMQPHeader());
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public AMQPHeaderInjectAction respondWithSASLPHeader() {
+        AMQPHeaderInjectAction response = new AMQPHeaderInjectAction(driver, AMQPHeader.getSASLHeader());
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public ByteBufferInjectAction respondWithBytes(byte[] buffer) {
+        ByteBufferInjectAction response = new ByteBufferInjectAction(driver, Unpooled.wrappedBuffer(buffer));
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public ByteBufferInjectAction respondWithBytes(ByteBuf buffer) {
+        ByteBufferInjectAction response = new ByteBufferInjectAction(driver, buffer);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    @Override
+    public void handleAMQPHeader(AMQPHeader header, AMQPTestDriver driver) {
+        assertThat("AMQP Header should match expected.", expected, equalTo(header));
+    }
+
+    @Override
+    public void handleSASLHeader(AMQPHeader header, AMQPTestDriver driver) {
+        assertThat("SASL Header should match expected.", expected, equalTo(header));
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AbstractExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AbstractExpectation.java
new file mode 100644
index 0000000..df1f038
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AbstractExpectation.java
@@ -0,0 +1,234 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.ScriptedExpectation;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslChallenge;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslInit;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslMechanisms;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslResponse;
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Close;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.HeartBeat;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+import org.apache.qpid.protonj2.test.driver.exceptions.UnexpectedPerformativeError;
+import org.hamcrest.Matcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Abstract base for expectations that need to handle matchers against fields in the
+ * value being expected.
+ *
+ * @param <T> The type being validated
+ */
+public abstract class AbstractExpectation<T extends ListDescribedType> implements ScriptedExpectation {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AMQPTestDriver.class);
+
+    public static int ANY_CHANNEL = -1;
+
+    protected int expectedChannel = ANY_CHANNEL;
+    protected final AMQPTestDriver driver;
+    private boolean optional;
+
+    public AbstractExpectation(AMQPTestDriver driver) {
+        this.driver = driver;
+    }
+
+    //----- Configure base expectations
+
+    public AbstractExpectation<T> onChannel(int channel) {
+        this.expectedChannel = channel;
+        return this;
+    }
+
+    /**
+     * @return true if this element represents an optional part of the script.
+     */
+    @Override
+    public boolean isOptional() {
+        return optional;
+    }
+
+    /**
+     * Marks this expectation as optional which can be useful when a frames arrival may or may not
+     * occur based on some other timing in the test.
+     *
+     * @return if the frame expectation is optional and its absence shouldn't fail the test.
+     */
+    public AbstractExpectation<T> optional() {
+        optional = true;
+        return this;
+    }
+
+    //------ Abstract classes use these methods to control validation
+
+    /**
+     * Verifies the fields of the performative against any matchers registered.
+     *
+     * @param performative
+     *      the performative received which will be validated against the configured matchers
+     *
+     * @throws AssertionError if a registered matcher assertion is not met.
+     */
+    protected final void verifyPerformative(T performative) throws AssertionError {
+        LOG.debug("About to check the fields of the performative." +
+                  "\n  Received:" + performative + "\n  Expectations: " + getExpectationMatcher());
+
+        assertThat("Performative does not match expectation", performative, getExpectationMatcher());
+    }
+
+    protected final void verifyPayload(ByteBuf payload) {
+        if (getPayloadMatcher() != null) {
+            assertThat("Paylod does not match expectation", payload, getPayloadMatcher());
+        } else if (payload != null) {
+            throw new AssertionError("Performative should not have been sent with a paylod: ");
+        }
+    }
+
+    protected final void verifyChannel(int channel) {
+        if (expectedChannel != ANY_CHANNEL && expectedChannel != channel) {
+            throw new AssertionError("Expected send on channel + " + expectedChannel + ": but was on channel:" + channel);
+        }
+    }
+
+    protected abstract Matcher<ListDescribedType> getExpectationMatcher();
+
+    protected abstract Class<T> getExpectedTypeClass();
+
+    protected Matcher<ByteBuf> getPayloadMatcher() {
+        return null;
+    }
+
+    //----- Base implementation of the handle methods to describe when we get wrong type.
+
+    @Override
+    public void handleOpen(Open open, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(open, payload, channel, context);
+    }
+
+    @Override
+    public void handleBegin(Begin begin, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(begin, payload, channel, context);
+    }
+
+    @Override
+    public void handleAttach(Attach attach, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(attach, payload, channel, context);
+    }
+
+    @Override
+    public void handleFlow(Flow flow, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(flow, payload, channel, context);
+    }
+
+    @Override
+    public void handleTransfer(Transfer transfer, ByteBuf payload, int channel,AMQPTestDriver context) {
+        doVerification(transfer, payload, channel, context);
+    }
+
+    @Override
+    public void handleDisposition(Disposition disposition, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(disposition, payload, channel, context);
+    }
+
+    @Override
+    public void handleDetach(Detach detach, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(detach, payload, channel, context);
+    }
+
+    @Override
+    public void handleEnd(End end, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(end, payload, channel, context);
+    }
+
+    @Override
+    public void handleClose(Close close, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(close, payload, channel, context);
+    }
+
+    @Override
+    public void handleHeartBeat(HeartBeat thump, ByteBuf payload, int channel, AMQPTestDriver context) {
+        doVerification(thump, payload, channel, context);
+    }
+
+    @Override
+    public void handleMechanisms(SaslMechanisms saslMechanisms, AMQPTestDriver context) {
+        doVerification(saslMechanisms, null, 0, context);
+    }
+
+    @Override
+    public void handleInit(SaslInit saslInit, AMQPTestDriver context) {
+        doVerification(saslInit, null, 0, context);
+    }
+
+    @Override
+    public void handleChallenge(SaslChallenge saslChallenge, AMQPTestDriver context) {
+        doVerification(saslChallenge, null, 0, context);
+    }
+
+    @Override
+    public void handleResponse(SaslResponse saslResponse, AMQPTestDriver context) {
+        doVerification(saslResponse, null, 0, context);
+    }
+
+    @Override
+    public void handleOutcome(SaslOutcome saslOutcome, AMQPTestDriver context) {
+        doVerification(saslOutcome, null, 0, context);
+    }
+
+    @Override
+    public void handleAMQPHeader(AMQPHeader header, AMQPTestDriver context) {
+        doVerification(header, null, 0, context);
+    }
+
+    @Override
+    public void handleSASLHeader(AMQPHeader header, AMQPTestDriver context) {
+        doVerification(header, null, 0, context);
+    }
+
+    //----- Internal implementation
+
+    private void doVerification(Object performative, ByteBuf payload, int channel, AMQPTestDriver driver) {
+        if (getExpectedTypeClass().equals(performative.getClass())) {
+            verifyPayload(payload);
+            verifyChannel(channel);
+            verifyPerformative(getExpectedTypeClass().cast(performative));
+        } else {
+            reportTypeExpectationError(performative, getExpectedTypeClass());
+        }
+    }
+
+    private void reportTypeExpectationError(Object received, Class<T> expected) {
+        throw new UnexpectedPerformativeError("Expeceted type: " + expected + " but received value: " + received);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AttachExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AttachExpectation.java
new file mode 100644
index 0000000..821e57f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/AttachExpectation.java
@@ -0,0 +1,803 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.LinkTracker;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.actions.AttachInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DetachInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.SenderSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.SourceMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.TargetMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transactions.CoordinatorMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.AttachMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP Attach performative
+ */
+public class AttachExpectation extends AbstractExpectation<Attach> {
+
+    private final AttachMatcher matcher = new AttachMatcher();
+
+    private AttachInjectAction response;
+    private boolean rejecting;
+
+    public AttachExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Configure default expectations for a valid Attach
+        withName(notNullValue());
+        withHandle(notNullValue());
+        withRole(notNullValue());
+    }
+
+    @Override
+    public AttachExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    public AttachInjectAction respond() {
+        response = new AttachInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public DetachInjectAction reject(boolean close, String condition, String description) {
+        return reject(close, Symbol.valueOf(condition), description);
+    }
+
+    public DetachInjectAction reject(boolean close, Symbol condition, String description) {
+        rejecting = true;
+        response = new AttachInjectAction(driver);
+        driver.addScriptedElement(response);
+
+        DetachInjectAction action =
+            new DetachInjectAction(driver).withClosed(close).withErrorCondition(condition, description);
+        driver.addScriptedElement(action);
+
+        return action;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleAttach(Attach attach, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleAttach(attach, payload, channel, context);
+
+        final UnsignedShort remoteChannel = UnsignedShort.valueOf(channel);
+        final SessionTracker session = driver.sessions().getSessionFromRemoteChannel(remoteChannel);
+
+        if (session == null) {
+            throw new AssertionError(String.format(
+                "Received Attach on channel [%d] that has no matching Session for that remote channel. ", remoteChannel));
+        }
+
+        final LinkTracker link = session.handleRemoteAttach(attach);
+
+        if (response != null) {
+            // Input was validated now populate response with auto values where not configured
+            // to say otherwise by the test.
+            if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+                response.onChannel(link.getSession().getLocalChannel());
+            }
+
+            // Populate the fields of the response with defaults if non set by the test script
+            if (response.getPerformative().getHandle() == null) {
+                response.withHandle(attach.getHandle());
+            }
+            if (response.getPerformative().getName() == null) {
+                response.withName(attach.getName());
+            }
+            if (response.getPerformative().getRole() == null) {
+                response.withRole(Boolean.TRUE.equals(attach.getRole()) ? Role.SENDER : Role.RECEIVER);
+            }
+            if (response.getPerformative().getSenderSettleMode() == null) {
+                response.withSndSettleMode(SenderSettleMode.valueOf(attach.getSenderSettleMode()));
+            }
+            if (response.getPerformative().getReceiverSettleMode() == null) {
+                response.withRcvSettleMode(ReceiverSettleMode.valueOf(attach.getReceiverSettleMode()));
+            }
+            if (response.getPerformative().getSource() == null && !response.isNullSourceRequired()) {
+                response.withSource(attach.getSource());
+                if (attach.getSource() != null && Boolean.TRUE.equals(attach.getSource().getDynamic())) {
+                    attach.getSource().setAddress(UUID.randomUUID().toString());
+                }
+            }
+
+            if (rejecting) {
+                if (Boolean.FALSE.equals(attach.getRole())) {
+                    // Sender attach so response should have null target
+                    response.withNullTarget();
+                } else {
+                    // Receiver attach so response should have null source
+                    response.withNullSource();
+                }
+            }
+
+            if (response.getPerformative().getTarget() == null && !response.isNullTargetRequired()) {
+                if (attach.getTarget() != null) {
+                    if (attach.getTarget() instanceof org.apache.qpid.protonj2.test.driver.codec.messaging.Target) {
+                        org.apache.qpid.protonj2.test.driver.codec.messaging.Target target =
+                            (org.apache.qpid.protonj2.test.driver.codec.messaging.Target) attach.getTarget();
+                        response.withTarget(target);
+                        if (target != null && Boolean.TRUE.equals(target.getDynamic())) {
+                            target.setAddress(UUID.randomUUID().toString());
+                        }
+                    } else {
+                        org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator coordinator =
+                            (org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator) attach.getTarget();
+                        response.withTarget(coordinator);
+                    }
+                }
+            }
+
+            if (response.getPerformative().getInitialDeliveryCount() == null) {
+                Role role = Role.valueOf(response.getPerformative().getRole());
+                if (role == Role.SENDER) {
+                    response.withInitialDeliveryCount(0);
+                }
+            }
+
+            // Other fields are left not set for now unless test script configured
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public AttachExpectation withName(String name) {
+        return withName(equalTo(name));
+    }
+
+    public AttachExpectation withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public AttachExpectation withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public AttachExpectation withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public AttachExpectation withRole(boolean role) {
+        return withRole(equalTo(role));
+    }
+
+    public AttachExpectation withRole(Boolean role) {
+        return withRole(equalTo(role));
+    }
+
+    public AttachExpectation withRole(Role role) {
+        return withRole(equalTo(role.getValue()));
+    }
+
+    public AttachExpectation ofSender() {
+        return withRole(equalTo(Role.SENDER.getValue()));
+    }
+
+    public AttachExpectation ofReceiver() {
+        return withRole(equalTo(Role.RECEIVER.getValue()));
+    }
+
+    public AttachExpectation withSndSettleMode(byte sndSettleMode) {
+        return withSndSettleMode(equalTo(UnsignedByte.valueOf(sndSettleMode)));
+    }
+
+    public AttachExpectation withSndSettleMode(Byte sndSettleMode) {
+        return withSndSettleMode(sndSettleMode == null ? nullValue() : equalTo(UnsignedByte.valueOf(sndSettleMode.byteValue())));
+    }
+
+    public AttachExpectation withSndSettleMode(SenderSettleMode sndSettleMode) {
+        return withSndSettleMode(sndSettleMode == null ? nullValue() : equalTo(sndSettleMode.getValue()));
+    }
+
+    public AttachExpectation withSenderSettleModeMixed() {
+        return withSndSettleMode(equalTo(SenderSettleMode.MIXED.getValue()));
+    }
+
+    public AttachExpectation withSenderSettleModeSettled() {
+        return withSndSettleMode(equalTo(SenderSettleMode.SETTLED.getValue()));
+    }
+
+    public AttachExpectation withSenderSettleModeUnsettled() {
+        return withSndSettleMode(equalTo(SenderSettleMode.UNSETTLED.getValue()));
+    }
+
+    public AttachExpectation withRcvSettleMode(byte rcvSettleMode) {
+        return withRcvSettleMode(equalTo(UnsignedByte.valueOf(rcvSettleMode)));
+    }
+
+    public AttachExpectation withRcvSettleMode(Byte rcvSettleMode) {
+        return withRcvSettleMode(rcvSettleMode == null ? nullValue() : equalTo(UnsignedByte.valueOf(rcvSettleMode.byteValue())));
+    }
+
+    public AttachExpectation withRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        return withRcvSettleMode(rcvSettleMode == null ? nullValue() : equalTo(rcvSettleMode.getValue()));
+    }
+
+    public AttachExpectation withReceivervSettlesFirst() {
+        return withRcvSettleMode(equalTo(ReceiverSettleMode.FIRST.getValue()));
+    }
+
+    public AttachExpectation withReceivervSettlesSecond() {
+        return withRcvSettleMode(equalTo(ReceiverSettleMode.SECOND.getValue()));
+    }
+
+    public AttachSourceMatcher withSource() {
+        AttachSourceMatcher matcher = new AttachSourceMatcher(this);
+        withSource(matcher);
+        return matcher;
+    }
+
+    public AttachTargetMatcher withTarget() {
+        AttachTargetMatcher matcher = new AttachTargetMatcher(this);
+        withTarget(matcher);
+        return matcher;
+    }
+
+    public AttachCoordinatorMatcher withCoordinator() {
+        AttachCoordinatorMatcher matcher = new AttachCoordinatorMatcher(this);
+        withCoordinator(matcher);
+        return matcher;
+    }
+
+    public AttachExpectation withSource(Source source) {
+        if (source != null) {
+            SourceMatcher sourceMatcher = new SourceMatcher(source);
+            return withSource(sourceMatcher);
+        } else {
+            return withSource(nullValue());
+        }
+    }
+
+    public AttachExpectation withTarget(Target target) {
+        if (target != null) {
+            TargetMatcher targetMatcher = new TargetMatcher(target);
+            return withTarget(targetMatcher);
+        } else {
+            return withTarget(nullValue());
+        }
+    }
+
+    public AttachExpectation withCoordinator(Coordinator coordinator) {
+        if (coordinator != null) {
+            CoordinatorMatcher coordinatorMatcher = new CoordinatorMatcher();
+            return withCoordinator(coordinatorMatcher);
+        } else {
+            return withCoordinator(nullValue());
+        }
+    }
+
+    public AttachExpectation withUnsettled(Map<Binary, DeliveryState> unsettled) {
+        // TODO - Need to match on the driver types for DeliveryState
+        return withUnsettled(equalTo(unsettled));
+    }
+
+    public AttachExpectation withIncompleteUnsettled(boolean incomplete) {
+        return withIncompleteUnsettled(equalTo(incomplete));
+    }
+
+    public AttachExpectation withInitialDeliveryCount(int initialDeliveryCount) {
+        return withInitialDeliveryCount(equalTo(UnsignedInteger.valueOf(initialDeliveryCount)));
+    }
+
+    public AttachExpectation withInitialDeliveryCount(long initialDeliveryCount) {
+        return withInitialDeliveryCount(equalTo(UnsignedInteger.valueOf(initialDeliveryCount)));
+    }
+
+    public AttachExpectation withInitialDeliveryCount(UnsignedInteger initialDeliveryCount) {
+        return withInitialDeliveryCount(equalTo(initialDeliveryCount));
+    }
+
+    public AttachExpectation withMaxMessageSize(long maxMessageSize) {
+        return withMaxMessageSize(equalTo(UnsignedLong.valueOf(maxMessageSize)));
+    }
+
+    public AttachExpectation withMaxMessageSize(UnsignedLong maxMessageSize) {
+        return withMaxMessageSize(equalTo(maxMessageSize));
+    }
+
+    public AttachExpectation withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public AttachExpectation withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public AttachExpectation withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public AttachExpectation withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public AttachExpectation withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public AttachExpectation withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public AttachExpectation withName(Matcher<?> m) {
+        matcher.withName(m);
+        return this;
+    }
+
+    public AttachExpectation withHandle(Matcher<?> m) {
+        matcher.withHandle(m);
+        return this;
+    }
+
+    public AttachExpectation withRole(Matcher<?> m) {
+        matcher.withRole(m);
+        return this;
+    }
+
+    public AttachExpectation withSndSettleMode(Matcher<?> m) {
+        matcher.withSndSettleMode(m);
+        return this;
+    }
+
+    public AttachExpectation withRcvSettleMode(Matcher<?> m) {
+        matcher.withRcvSettleMode(m);
+        return this;
+    }
+
+    public AttachExpectation withSource(Matcher<?> m) {
+        matcher.withSource(m);
+        return this;
+    }
+
+    public AttachExpectation withTarget(Matcher<?> m) {
+        matcher.withTarget(m);
+        return this;
+    }
+
+    public AttachExpectation withCoordinator(Matcher<?> m) {
+        matcher.withCoordinator(m);
+        return this;
+    }
+
+    public AttachExpectation withUnsettled(Matcher<?> m) {
+        matcher.withUnsettled(m);
+        return this;
+    }
+
+    public AttachExpectation withIncompleteUnsettled(Matcher<?> m) {
+        matcher.withIncompleteUnsettled(m);
+        return this;
+    }
+
+    public AttachExpectation withInitialDeliveryCount(Matcher<?> m) {
+        matcher.withInitialDeliveryCount(m);
+        return this;
+    }
+
+    public AttachExpectation withMaxMessageSize(Matcher<?> m) {
+        matcher.withMaxMessageSize(m);
+        return this;
+    }
+
+    public AttachExpectation withOfferedCapabilities(Matcher<?> m) {
+        matcher.withOfferedCapabilities(m);
+        return this;
+    }
+
+    public AttachExpectation withDesiredCapabilities(Matcher<?> m) {
+        matcher.withDesiredCapabilities(m);
+        return this;
+    }
+
+    public AttachExpectation withProperties(Matcher<?> m) {
+        matcher.withProperties(m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Attach> getExpectedTypeClass() {
+        return Attach.class;
+    }
+
+    //----- Extend the Source and Target Matchers to extend the API
+
+    public static class AttachSourceMatcher extends SourceMatcher {
+
+        private final AttachExpectation expectation;
+
+        public AttachSourceMatcher(AttachExpectation expectation) {
+            this.expectation = expectation;
+        }
+
+        public AttachExpectation also() {
+            return expectation;
+        }
+
+        public AttachExpectation and() {
+            return expectation;
+        }
+
+        @Override
+        public AttachSourceMatcher withAddress(String name) {
+            super.withAddress(name);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDurable(TerminusDurability durability) {
+            super.withDurable(durability);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withExpiryPolicy(TerminusExpiryPolicy expiry) {
+            super.withExpiryPolicy(expiry);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withTimeout(int timeout) {
+            super.withTimeout(timeout);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withTimeout(long timeout) {
+            super.withTimeout(timeout);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withTimeout(UnsignedInteger timeout) {
+            super.withTimeout(timeout);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDynamic(boolean dynamic) {
+            super.withDynamic(dynamic);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDynamicNodePropertiesMap(Map<Symbol, Object> properties) {
+            super.withDynamicNodePropertiesMap(properties);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDynamicNodeProperties(Map<String, Object> properties) {
+            super.withDynamicNodeProperties(properties);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDistributionMode(String distributionMode) {
+            super.withDistributionMode(distributionMode);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDistributionMode(Symbol distributionMode) {
+            super.withDistributionMode(distributionMode);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withFilter(Map<String, Object> filter) {
+            super.withFilter(filter);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDefaultOutcome(DeliveryState defaultOutcome) {
+            super.withDefaultOutcome(defaultOutcome);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withOutcomes(String... outcomes) {
+            super.withOutcomes(outcomes);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withOutcomes(Symbol... outcomes) {
+            super.withOutcomes(outcomes);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withCapabilities(String... capabilities) {
+            super.withCapabilities(capabilities);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withCapabilities(Symbol... capabilities) {
+            super.withCapabilities(capabilities);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withAddress(Matcher<?> m) {
+            super.withAddress(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDurable(Matcher<?> m) {
+            super.withDurable(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withExpiryPolicy(Matcher<?> m) {
+            super.withExpiryPolicy(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withTimeout(Matcher<?> m) {
+            super.withTimeout(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDefaultTimeout() {
+            super.withTimeout(anyOf(nullValue(), equalTo(UnsignedInteger.ZERO)));
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDynamic(Matcher<?> m) {
+            super.withDynamic(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDynamicNodeProperties(Matcher<?> m) {
+            super.withDynamicNodeProperties(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDistributionMode(Matcher<?> m) {
+            super.withDistributionMode(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withFilter(Matcher<?> m) {
+            super.withFilter(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withDefaultOutcome(Matcher<?> m) {
+            super.withDefaultOutcome(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withOutcomes(Matcher<?> m) {
+            super.withOutcomes(m);
+            return this;
+        }
+
+        @Override
+        public AttachSourceMatcher withCapabilities(Matcher<?> m) {
+            super.withCapabilities(m);
+            return this;
+        }
+    }
+
+    public static class AttachTargetMatcher extends TargetMatcher {
+
+        private final AttachExpectation expectation;
+
+        public AttachTargetMatcher(AttachExpectation expectation) {
+            this.expectation = expectation;
+        }
+
+        public AttachExpectation also() {
+            return expectation;
+        }
+
+        public AttachExpectation and() {
+            return expectation;
+        }
+
+        @Override
+        public AttachTargetMatcher withAddress(String name) {
+            super.withAddress(name);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDurable(TerminusDurability durability) {
+            super.withDurable(durability);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withExpiryPolicy(TerminusExpiryPolicy expiry) {
+            super.withExpiryPolicy(expiry);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withTimeout(int timeout) {
+            super.withTimeout(timeout);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withTimeout(long timeout) {
+            super.withTimeout(timeout);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withTimeout(UnsignedInteger timeout) {
+            super.withTimeout(timeout);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDefaultTimeout() {
+            super.withDefaultTimeout();
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDynamic(boolean dynamic) {
+            super.withDynamic(dynamic);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDynamicNodeProperties(Map<String, Object> properties) {
+            super.withDynamicNodeProperties(properties);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withCapabilities(Symbol... capabilities) {
+            super.withCapabilities(capabilities);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withCapabilities(String... capabilities) {
+            super.withCapabilities(capabilities);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withAddress(Matcher<?> m) {
+            super.withAddress(m);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDurable(Matcher<?> m) {
+            super.withDurable(m);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withExpiryPolicy(Matcher<?> m) {
+            super.withExpiryPolicy(m);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withTimeout(Matcher<?> m) {
+            super.withTimeout(m);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDynamic(Matcher<?> m) {
+            super.withDynamic(m);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withDynamicNodeProperties(Matcher<?> m) {
+            super.withDynamicNodeProperties(m);
+            return this;
+        }
+
+        @Override
+        public AttachTargetMatcher withCapabilities(Matcher<?> m) {
+            super.withCapabilities(m);
+            return this;
+        }
+    }
+
+    public static class AttachCoordinatorMatcher extends CoordinatorMatcher {
+
+        private final AttachExpectation expectation;
+
+        public AttachCoordinatorMatcher(AttachExpectation expectation) {
+            this.expectation = expectation;
+        }
+
+        public AttachExpectation also() {
+            return expectation;
+        }
+
+        public AttachExpectation and() {
+            return expectation;
+        }
+
+        @Override
+        public AttachCoordinatorMatcher withCapabilities(Symbol... capabilities) {
+            super.withCapabilities(capabilities);
+            return this;
+        }
+
+        @Override
+        public AttachCoordinatorMatcher withCapabilities(String... capabilities) {
+            super.withCapabilities(capabilities);
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/BeginExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/BeginExpectation.java
new file mode 100644
index 0000000..6750d9b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/BeginExpectation.java
@@ -0,0 +1,237 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.EndInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.BeginMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP Begin performative
+ */
+public class BeginExpectation extends AbstractExpectation<Begin> {
+
+    private final BeginMatcher matcher = new BeginMatcher();
+
+    private BeginInjectAction response;
+
+    public BeginExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Configure default expectations for mandatory fields
+        withRemoteChannel(anyOf(nullValue(), notNullValue()));
+        withNextOutgoingId(notNullValue());
+        withIncomingWindow(notNullValue());
+        withOutgoingWindow(notNullValue());
+    }
+
+    @Override
+    public BeginExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    @Override
+    public BeginExpectation optional() {
+        super.optional();
+        return this;
+    }
+
+    public BeginInjectAction respond() {
+        response = new BeginInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public EndInjectAction reject(String condition, String description) {
+        return reject(Symbol.valueOf(condition), description);
+    }
+
+    public EndInjectAction reject(Symbol condition, String description) {
+        response = new BeginInjectAction(driver);
+        driver.addScriptedElement(response);
+
+        EndInjectAction endAction = new EndInjectAction(driver).withErrorCondition(condition, description);
+        driver.addScriptedElement(endAction);
+
+        return endAction;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleBegin(Begin begin, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleBegin(begin, payload, channel, context);
+
+        context.sessions().handleBegin(begin, UnsignedShort.valueOf(channel));
+
+        if (response != null) {
+            response.withRemoteChannel(channel);
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public BeginExpectation withRemoteChannel(int remoteChannel) {
+        return withRemoteChannel(equalTo(UnsignedShort.valueOf((short) remoteChannel)));
+    }
+
+    public BeginExpectation withRemoteChannel(UnsignedShort remoteChannel) {
+        return withRemoteChannel(equalTo(remoteChannel));
+    }
+
+    public BeginExpectation withNextOutgoingId(int nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public BeginExpectation withNextOutgoingId(long nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public BeginExpectation withNextOutgoingId(UnsignedInteger nextOutgoingId) {
+        return withNextOutgoingId(equalTo(nextOutgoingId));
+    }
+
+    public BeginExpectation withIncomingWindow(int incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public BeginExpectation withIncomingWindow(long incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public BeginExpectation withIncomingWindow(UnsignedInteger incomingWindow) {
+        return withIncomingWindow(equalTo(incomingWindow));
+    }
+
+    public BeginExpectation withOutgoingWindow(int outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public BeginExpectation withOutgoingWindow(long outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public BeginExpectation withOutgoingWindow(UnsignedInteger outgoingWindow) {
+        return withOutgoingWindow(equalTo(outgoingWindow));
+    }
+
+    public BeginExpectation withHandleMax(int handleMax) {
+        return withHandleMax(equalTo(UnsignedInteger.valueOf(handleMax)));
+    }
+
+    public BeginExpectation withHandleMax(long handleMax) {
+        return withHandleMax(equalTo(UnsignedInteger.valueOf(handleMax)));
+    }
+
+    public BeginExpectation withHandleMax(UnsignedInteger handleMax) {
+        return withHandleMax(equalTo(handleMax));
+    }
+
+    public BeginExpectation withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public BeginExpectation withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public BeginExpectation withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public BeginExpectation withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public BeginExpectation withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public BeginExpectation withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public BeginExpectation withRemoteChannel(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.REMOTE_CHANNEL, m);
+        return this;
+    }
+
+    public BeginExpectation withNextOutgoingId(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.NEXT_OUTGOING_ID, m);
+        return this;
+    }
+
+    public BeginExpectation withIncomingWindow(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.INCOMING_WINDOW, m);
+        return this;
+    }
+
+    public BeginExpectation withOutgoingWindow(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.OUTGOING_WINDOW, m);
+        return this;
+    }
+
+    public BeginExpectation withHandleMax(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.HANDLE_MAX, m);
+        return this;
+    }
+
+    public BeginExpectation withOfferedCapabilities(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.OFFERED_CAPABILITIES, m);
+        return this;
+    }
+
+    public BeginExpectation withDesiredCapabilities(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.DESIRED_CAPABILITIES, m);
+        return this;
+    }
+
+    public BeginExpectation withProperties(Matcher<?> m) {
+        matcher.addFieldMatcher(Begin.Field.PROPERTIES, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Begin> getExpectedTypeClass() {
+        return Begin.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/CloseExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/CloseExpectation.java
new file mode 100644
index 0000000..1240467
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/CloseExpectation.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.CloseInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Close;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.CloseMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP Close performative
+ */
+public class CloseExpectation extends AbstractExpectation<Close> {
+
+    private final CloseMatcher matcher = new CloseMatcher();
+
+    private CloseInjectAction response;
+
+    public CloseExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public CloseInjectAction respond() {
+        response = new CloseInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleClose(Close close, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleClose(close, payload, channel, context);
+
+        if (response == null) {
+            return;
+        }
+
+        // Input was validated now populate response with auto values where not configured
+        // to say otherwise by the test.
+        if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+            response.onChannel(channel);
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public CloseExpectation withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public CloseExpectation withError(String condition, String description) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description)));
+    }
+
+    public CloseExpectation withError(String condition, String description, Map<String, Object> info) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info))));
+    }
+
+    public CloseExpectation withError(Symbol condition, String description) {
+        return withError(equalTo(new ErrorCondition(condition, description)));
+    }
+
+    public CloseExpectation withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(equalTo(new ErrorCondition(condition, description, info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public CloseExpectation withError(Matcher<?> m) {
+        matcher.addFieldMatcher(Close.Field.ERROR, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Close> getExpectedTypeClass() {
+        return Close.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DeclareExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DeclareExpectation.java
new file mode 100644
index 0000000..a0270b8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DeclareExpectation.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import java.util.Random;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.actions.DispositionInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declare;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declared;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher;
+
+/**
+ * Expectation used to script incoming transaction declarations.
+ */
+public class DeclareExpectation extends TransferExpectation {
+
+    private final EncodedAmqpValueMatcher defaultPayloadMatcher = new EncodedAmqpValueMatcher(new Declare());
+
+    public DeclareExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        withPayload(defaultPayloadMatcher);
+    }
+
+    @Override
+    public DispositionInjectAction accept() {
+        final byte[] txnId = new byte[4];
+
+        Random rand = new Random();
+        rand.setSeed(System.nanoTime());
+        rand.nextBytes(txnId);
+
+        return accept(txnId);
+    }
+
+    public DispositionInjectAction accept(byte[] txnId) {
+        response = new DispositionInjectAction(driver);
+        response.withSettled(true);
+        if (txnId != null) {
+            response.withState(new Declared().setTxnId(new Binary(txnId)));
+        } else {
+            response.withState(new Declared());
+        }
+
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    @Override
+    public DeclareExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    public DeclareExpectation withDeclare(Declare declare) {
+        withPayload(new EncodedAmqpValueMatcher(declare));
+        return this;
+    }
+
+    public DeclareExpectation withNullDeclare() {
+        withPayload(new EncodedAmqpValueMatcher(null));
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DetachExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DetachExpectation.java
new file mode 100644
index 0000000..00f4c21
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DetachExpectation.java
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.LinkTracker;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DetachInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.DetachMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP Detach performative
+ */
+public class DetachExpectation extends AbstractExpectation<Detach> {
+
+    private final DetachMatcher matcher = new DetachMatcher();
+
+    private DetachInjectAction response;
+
+    public DetachExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Default validation of mandatory fields
+        withHandle(notNullValue());
+    }
+
+    @Override
+    public DetachExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    public DetachInjectAction respond() {
+        response = new DetachInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleDetach(Detach detach, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleDetach(detach, payload, channel, context);
+
+        final UnsignedShort remoteChannel = UnsignedShort.valueOf(channel);
+        final SessionTracker session = driver.sessions().getSessionFromRemoteChannel(remoteChannel);
+
+        if (session == null) {
+            throw new AssertionError(String.format(
+                "Received Detach on channel [%d] that has no matching Session for that remote channel. ", remoteChannel));
+        }
+
+        final LinkTracker link = session.handleRemoteDetach(detach);
+
+        if (response != null) {
+            // Input was validated now populate response with auto values where not configured
+            // to say otherwise by the test.
+            if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+                response.onChannel(link.getSession().getLocalChannel());
+            }
+
+            if (response.getPerformative().getHandle() == null) {
+                response.withHandle(detach.getHandle());
+            }
+
+            if (response.getPerformative().getClosed() == null) {
+                response.withClosed(detach.getClosed());
+            }
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public DetachExpectation withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public DetachExpectation withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public DetachExpectation withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public DetachExpectation withClosed(boolean closed) {
+        return withClosed(equalTo(closed));
+    }
+
+    public DetachExpectation withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public DetachExpectation withError(String condition, String description) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description)));
+    }
+
+    public DetachExpectation withError(String condition, String description, Map<String, Object> info) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info))));
+    }
+
+    public DetachExpectation withError(Symbol condition, String description) {
+        return withError(equalTo(new ErrorCondition(condition, description)));
+    }
+
+    public DetachExpectation withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(equalTo(new ErrorCondition(condition, description, info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public DetachExpectation withHandle(Matcher<?> m) {
+        matcher.addFieldMatcher(Detach.Field.HANDLE, m);
+        return this;
+    }
+
+    public DetachExpectation withClosed(Matcher<?> m) {
+        matcher.addFieldMatcher(Detach.Field.CLOSED, m);
+        return this;
+    }
+
+    public DetachExpectation withError(Matcher<?> m) {
+        matcher.addFieldMatcher(Detach.Field.ERROR, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Detach> getExpectedTypeClass() {
+        return Detach.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DischargeExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DischargeExpectation.java
new file mode 100644
index 0000000..cf6e431
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DischargeExpectation.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Discharge;
+import org.apache.qpid.protonj2.test.driver.matchers.transactions.DischargeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher;
+
+/**
+ * Expectation used to script incoming transaction declarations.
+ */
+public class DischargeExpectation extends TransferExpectation {
+
+    private DischargeMatcher discharge = new DischargeMatcher();
+    private EncodedAmqpValueMatcher matcher = new EncodedAmqpValueMatcher(discharge);
+
+    public DischargeExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        withPayload(matcher);
+    }
+
+    @Override
+    public DischargeExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    public DischargeExpectation withFail(boolean fail) {
+        discharge.withFail(fail);
+        return this;
+    }
+
+    public DischargeExpectation withTxnId(byte[] txnId) {
+        discharge.withTxnId(new Binary(txnId));
+        return this;
+    }
+
+    public DischargeExpectation withTxnId(Binary txnId) {
+        discharge.withTxnId(txnId);
+        return this;
+    }
+
+    public DischargeExpectation withDischarge(Discharge discharge) {
+        withPayload(new EncodedAmqpValueMatcher(discharge));
+        return this;
+    }
+
+    public DischargeExpectation withNullDischarge() {
+        withPayload(new EncodedAmqpValueMatcher(null));
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DispositionExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DispositionExpectation.java
new file mode 100644
index 0000000..aea0be7
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/DispositionExpectation.java
@@ -0,0 +1,291 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Rejected;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.matchers.transactions.TransactionalStateMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.DispositionMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Scripted expectation for the AMQP Disposition performative
+ */
+public class DispositionExpectation extends AbstractExpectation<Disposition> {
+
+    private final DispositionMatcher matcher = new DispositionMatcher();
+    private final DeliveryStateBuilder stateBuilder = new DeliveryStateBuilder();
+
+    public DispositionExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Default mandatory field validation
+        withRole(notNullValue());
+        withFirst(notNullValue());
+    }
+
+    @Override
+    public DispositionExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public DispositionExpectation withRole(boolean role) {
+        withRole(equalTo(role));
+        return this;
+    }
+
+    public DispositionExpectation withRole(Boolean role) {
+        withRole(equalTo(role));
+        return this;
+    }
+
+    public DispositionExpectation withRole(Role role) {
+        withRole(equalTo(role.getValue()));
+        return this;
+    }
+
+    public DispositionExpectation withFirst(int first) {
+        return withFirst(equalTo(UnsignedInteger.valueOf(first)));
+    }
+
+    public DispositionExpectation withFirst(long first) {
+        return withFirst(equalTo(UnsignedInteger.valueOf(first)));
+    }
+
+    public DispositionExpectation withFirst(UnsignedInteger first) {
+        return withFirst(equalTo(first));
+    }
+
+    public DispositionExpectation withLast(int last) {
+        return withLast(equalTo(UnsignedInteger.valueOf(last)));
+    }
+
+    public DispositionExpectation withLast(long last) {
+        return withLast(equalTo(UnsignedInteger.valueOf(last)));
+    }
+
+    public DispositionExpectation withLast(UnsignedInteger last) {
+        return withLast(equalTo(last));
+    }
+
+    public DispositionExpectation withSettled(boolean settled) {
+        return withSettled(equalTo(settled));
+    }
+
+    public DispositionExpectation withState(DeliveryState state) {
+        return withState(equalTo(state));
+    }
+
+    public DeliveryStateBuilder withState() {
+        return stateBuilder;
+    }
+
+    public DispositionExpectation withBatchable(boolean batchable) {
+        return withBatchable(equalTo(batchable));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public DispositionExpectation withRole(Matcher<?> m) {
+        matcher.addFieldMatcher(Disposition.Field.ROLE, m);
+        return this;
+    }
+
+    public DispositionExpectation withFirst(Matcher<?> m) {
+        matcher.addFieldMatcher(Disposition.Field.FIRST, m);
+        return this;
+    }
+
+    public DispositionExpectation withLast(Matcher<?> m) {
+        matcher.addFieldMatcher(Disposition.Field.LAST, m);
+        return this;
+    }
+
+    public DispositionExpectation withSettled(Matcher<?> m) {
+        matcher.addFieldMatcher(Disposition.Field.SETTLED, m);
+        return this;
+    }
+
+    public DispositionExpectation withState(Matcher<?> m) {
+        matcher.addFieldMatcher(Disposition.Field.STATE, m);
+        return this;
+    }
+
+    public DispositionExpectation withBatchable(Matcher<?> m) {
+        matcher.addFieldMatcher(Disposition.Field.BATCHABLE, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Disposition> getExpectedTypeClass() {
+        return Disposition.class;
+    }
+
+    public final class DeliveryStateBuilder {
+
+        public DispositionExpectation accepted() {
+            withState(Accepted.getInstance());
+            return DispositionExpectation.this;
+        }
+
+        public DispositionExpectation released() {
+            withState(Released.getInstance());
+            return DispositionExpectation.this;
+        }
+
+        public DispositionExpectation rejected() {
+            withState(new Rejected());
+            return DispositionExpectation.this;
+        }
+
+        public DispositionExpectation rejected(String condition, String description) {
+            withState(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return DispositionExpectation.this;
+        }
+
+        public DispositionExpectation modified() {
+            withState(new Modified());
+            return DispositionExpectation.this;
+        }
+
+        public DispositionExpectation modified(boolean failed) {
+            withState(new Modified());
+            return DispositionExpectation.this;
+        }
+
+        public DispositionExpectation modified(boolean failed, boolean undeliverableHere) {
+            withState(new Modified());
+            return DispositionExpectation.this;
+        }
+
+        public DispositionTransactionalStateMatcher transactional() {
+            DispositionTransactionalStateMatcher matcher = new DispositionTransactionalStateMatcher(DispositionExpectation.this);
+            withState(matcher);
+            return matcher;
+        }
+    }
+
+    //----- Extend the TransactionalStateMatcher type to have an API suitable for Transfer expectation setup
+
+    public static class DispositionTransactionalStateMatcher extends TransactionalStateMatcher {
+
+        private final DispositionExpectation expectation;
+
+        public DispositionTransactionalStateMatcher(DispositionExpectation expectation) {
+            this.expectation = expectation;
+        }
+
+        public DispositionExpectation also() {
+            return expectation;
+        }
+
+        public DispositionExpectation and() {
+            return expectation;
+        }
+
+        @Override
+        public DispositionTransactionalStateMatcher withTxnId(byte[] txnId) {
+            super.withTxnId(equalTo(new Binary(txnId)));
+            return this;
+        }
+
+        @Override
+        public DispositionTransactionalStateMatcher withTxnId(Binary txnId) {
+            super.withTxnId(equalTo(txnId));
+            return this;
+        }
+
+        @Override
+        public DispositionTransactionalStateMatcher withOutcome(DeliveryState outcome) {
+            super.withOutcome(equalTo(outcome));
+            return this;
+        }
+
+        //----- Matcher based with methods for more complex validation
+
+        @Override
+        public DispositionTransactionalStateMatcher withTxnId(Matcher<?> m) {
+            super.withOutcome(m);
+            return this;
+        }
+
+        @Override
+        public DispositionTransactionalStateMatcher withOutcome(Matcher<?> m) {
+            super.withOutcome(m);
+            return this;
+        }
+
+        // ----- Add a layer to allow configuring the outcome without specific type dependencies
+
+        public DispositionTransactionalStateMatcher withAccepted() {
+            super.withOutcome(Accepted.getInstance());
+            return this;
+        }
+
+        public DispositionTransactionalStateMatcher withReleased() {
+            super.withOutcome(Released.getInstance());
+            return this;
+        }
+
+        public DispositionTransactionalStateMatcher withRejected() {
+            super.withOutcome(new Rejected());
+            return this;
+        }
+
+        public DispositionTransactionalStateMatcher withRejected(String condition, String description) {
+            super.withOutcome(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return this;
+        }
+
+        public DispositionTransactionalStateMatcher withModified() {
+            super.withOutcome(new Modified());
+            return this;
+        }
+
+        public DispositionTransactionalStateMatcher withModified(boolean failed) {
+            super.withOutcome(new Modified().setDeliveryFailed(failed));
+            return this;
+        }
+
+        public DispositionTransactionalStateMatcher withModified(boolean failed, boolean undeliverableHere) {
+            super.withOutcome(new Modified().setDeliveryFailed(failed).setUndeliverableHere(undeliverableHere));
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/EmptyFrameExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/EmptyFrameExpectation.java
new file mode 100644
index 0000000..b29a027
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/EmptyFrameExpectation.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.actions.EmptyFrameInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.HeartBeat;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.HeartBeatMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Expectation that asserts an Empty AMQP frame arrived (heart beat).
+ */
+public class EmptyFrameExpectation extends AbstractExpectation<HeartBeat> {
+
+    private final HeartBeatMatcher matcher = new HeartBeatMatcher();
+
+    public EmptyFrameExpectation(AMQPTestDriver driver) {
+        super(driver);
+        onChannel(0);
+    }
+
+    @Override
+    public EmptyFrameExpectation onChannel(int channel) {
+        if (channel != 0) throw new IllegalArgumentException("Empty Frames must arrive on channel zero");
+        super.onChannel(0);
+        return this;
+    }
+
+    public EmptyFrameInjectAction respond() {
+        EmptyFrameInjectAction response = new EmptyFrameInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<HeartBeat> getExpectedTypeClass() {
+        return HeartBeat.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/EndExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/EndExpectation.java
new file mode 100644
index 0000000..b4c5e3a
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/EndExpectation.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.EndInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.EndMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP End performative
+ */
+public class EndExpectation extends AbstractExpectation<End> {
+
+    private final EndMatcher matcher = new EndMatcher();
+
+    private EndInjectAction response;
+
+    public EndExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    @Override
+    public EndExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    public EndInjectAction respond() {
+        response = new EndInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleEnd(End end, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleEnd(end, payload, channel, context);
+
+        // Ensure that local session tracking knows that remote ended a Session.
+        final SessionTracker session = context.sessions().handleEnd(end, UnsignedShort.valueOf(channel));
+
+        if (response != null) {
+            if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+                response.onChannel(session.getLocalChannel());
+            }
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public EndExpectation withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public EndExpectation withError(String condition, String description) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description)));
+    }
+
+    public EndExpectation withError(String condition, String description, Map<String, Object> info) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info))));
+    }
+
+    public EndExpectation withError(Symbol condition, String description) {
+        return withError(equalTo(new ErrorCondition(condition, description)));
+    }
+
+    public EndExpectation withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(equalTo(new ErrorCondition(condition, description, info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public EndExpectation withError(Matcher<?> m) {
+        matcher.addFieldMatcher(End.Field.ERROR, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<End> getExpectedTypeClass() {
+        return End.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/FlowExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/FlowExpectation.java
new file mode 100644
index 0000000..ed1d98c
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/FlowExpectation.java
@@ -0,0 +1,300 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.LinkTracker;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.FlowInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.FlowMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP Flow performative
+ */
+public class FlowExpectation extends AbstractExpectation<Flow> {
+
+    private final FlowMatcher matcher = new FlowMatcher();
+    private FlowInjectAction response;
+
+    public FlowExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Default expectations for mandatory performative fields.
+        withNextIncomingId(anyOf(nullValue(), notNullValue()));
+        withIncomingWindow(notNullValue());
+        withNextOutgoingId(notNullValue());
+        withOutgoingWindow(notNullValue());
+    }
+
+    @Override
+    public FlowExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    public FlowInjectAction respond() {
+        response = new FlowInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleFlow(Flow flow, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleFlow(flow, payload, channel, context);
+
+        final UnsignedShort remoteChannel = UnsignedShort.valueOf(channel);
+        final SessionTracker session = driver.sessions().getSessionFromRemoteChannel(remoteChannel);
+
+        if (session == null) {
+            throw new AssertionError(String.format(
+                "Received Detach on channel [%d] that has no matching Session for that remote channel. ", remoteChannel));
+        }
+
+        final LinkTracker linkTracker = session.handleFlow(flow);  // Can be null if Flow was session level only.
+
+        if (response != null) {
+            // Input was validated now populate response with auto values where not configured
+            // to say otherwise by the test.
+            if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+                response.onChannel(session.getLocalChannel());
+            }
+
+            // TODO: The auto response values need to be pulled from session activity to produce meaningful auto
+            //       generated values for scripted responses.
+
+            // Populate the fields of the response with defaults if non set by the test script
+            if (response.getPerformative().getNextIncomingId() == null) {
+                response.withNextIncomingId(flow.getNextOutgoingId().longValue()); //TODO: this could be wrong, need to know about the transfers received (and sent by peer).
+            }
+
+            if (response.getPerformative().getIncomingWindow() == null) {
+                response.withIncomingWindow(Integer.MAX_VALUE); //TODO: shouldnt be hard coded
+            }
+
+            if (response.getPerformative().getNextOutgoingId() == null) {
+                response.withNextOutgoingId(flow.getNextIncomingId().longValue()); //TODO: this could be wrong, need to know about the transfers sent (and received at recipient peer).
+            }
+
+            if (response.getPerformative().getOutgoingWindow() == null) {
+                response.withOutgoingWindow(0); //TODO: shouldnt be hard coded, session might have senders on it as well as receivers
+            }
+
+            if (response.getPerformative().getHandle() == null && linkTracker != null) {
+                response.withHandle(linkTracker.getHandle()); //TODO: this is wrong, need a lookup for the local link and then get its remote handle.
+            }
+
+            // TODO: blow up on response if credit not populated?
+
+            // Other fields are left not set for now unless test script configured
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public FlowExpectation withNextIncomingId(int nextIncomingId) {
+        return withNextIncomingId(equalTo(UnsignedInteger.valueOf(nextIncomingId)));
+    }
+
+    public FlowExpectation withNextIncomingId(long nextIncomingId) {
+        return withNextIncomingId(equalTo(UnsignedInteger.valueOf(nextIncomingId)));
+    }
+
+    public FlowExpectation withNextIncomingId(UnsignedInteger nextIncomingId) {
+        return withNextIncomingId(equalTo(nextIncomingId));
+    }
+
+    public FlowExpectation withIncomingWindow(int incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public FlowExpectation withIncomingWindow(long incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public FlowExpectation withIncomingWindow(UnsignedInteger incomingWindow) {
+        return withIncomingWindow(equalTo(incomingWindow));
+    }
+
+    public FlowExpectation withNextOutgoingId(int nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public FlowExpectation withNextOutgoingId(long nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public FlowExpectation withNextOutgoingId(UnsignedInteger nextOutgoingId) {
+        return withNextOutgoingId(equalTo(nextOutgoingId));
+    }
+
+    public FlowExpectation withOutgoingWindow(int outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public FlowExpectation withOutgoingWindow(long outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public FlowExpectation withOutgoingWindow(UnsignedInteger outgoingWindow) {
+        return withOutgoingWindow(equalTo(outgoingWindow));
+    }
+
+    public FlowExpectation withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public FlowExpectation withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public FlowExpectation withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public FlowExpectation withDeliveryCount(int deliveryCount) {
+        return withDeliveryCount(equalTo(UnsignedInteger.valueOf(deliveryCount)));
+    }
+
+    public FlowExpectation withDeliveryCount(long deliveryCount) {
+        return withDeliveryCount(equalTo(UnsignedInteger.valueOf(deliveryCount)));
+    }
+
+    public FlowExpectation withDeliveryCount(UnsignedInteger deliveryCount) {
+        return withDeliveryCount(equalTo(deliveryCount));
+    }
+
+    public FlowExpectation withLinkCredit(int linkCredit) {
+        return withLinkCredit(equalTo(UnsignedInteger.valueOf(linkCredit)));
+    }
+
+    public FlowExpectation withLinkCredit(long linkCredit) {
+        return withLinkCredit(equalTo(UnsignedInteger.valueOf(linkCredit)));
+    }
+
+    public FlowExpectation withLinkCredit(UnsignedInteger linkCredit) {
+        return withLinkCredit(equalTo(linkCredit));
+    }
+
+    public FlowExpectation withAvailable(int available) {
+        return withAvailable(equalTo(UnsignedInteger.valueOf(available)));
+    }
+
+    public FlowExpectation withAvailable(long available) {
+        return withAvailable(equalTo(UnsignedInteger.valueOf(available)));
+    }
+
+    public FlowExpectation withAvailable(UnsignedInteger available) {
+        return withAvailable(equalTo(available));
+    }
+
+    public FlowExpectation withDrain(boolean drain) {
+        return withDrain(equalTo(drain));
+    }
+
+    public FlowExpectation withEcho(boolean echo) {
+        return withEcho(equalTo(echo));
+    }
+
+    public FlowExpectation withProperties(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public FlowExpectation withNextIncomingId(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.NEXT_INCOMING_ID, m);
+        return this;
+    }
+
+    public FlowExpectation withIncomingWindow(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.INCOMING_WINDOW, m);
+        return this;
+    }
+
+    public FlowExpectation withNextOutgoingId(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.NEXT_OUTGOING_ID, m);
+        return this;
+    }
+
+    public FlowExpectation withOutgoingWindow(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.OUTGOING_WINDOW, m);
+        return this;
+    }
+
+    public FlowExpectation withHandle(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.HANDLE, m);
+        return this;
+    }
+
+    public FlowExpectation withDeliveryCount(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.DELIVERY_COUNT, m);
+        return this;
+    }
+
+    public FlowExpectation withLinkCredit(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.LINK_CREDIT, m);
+        return this;
+    }
+
+    public FlowExpectation withAvailable(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.AVAILABLE, m);
+        return this;
+    }
+
+    public FlowExpectation withDrain(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.DRAIN, m);
+        return this;
+    }
+
+    public FlowExpectation withEcho(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.ECHO, m);
+        return this;
+    }
+
+    public FlowExpectation withProperties(Matcher<?> m) {
+        matcher.addFieldMatcher(Flow.Field.PROPERTIES, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Flow> getExpectedTypeClass() {
+        return Flow.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/OpenExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/OpenExpectation.java
new file mode 100644
index 0000000..30ebdbc
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/OpenExpectation.java
@@ -0,0 +1,263 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.CloseInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.OpenInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.OpenMatcher;
+import org.hamcrest.Matcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Scripted expectation for the AMQP Open performative
+ */
+public class OpenExpectation extends AbstractExpectation<Open> {
+
+    private final OpenMatcher matcher = new OpenMatcher();
+
+    private OpenInjectAction response;
+    private boolean explicitlyNullContainerId;
+
+    public OpenExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Validate mandatory field by default
+        withContainerId(notNullValue());
+
+        onChannel(0);  // Open must used channel zero.
+    }
+
+    public OpenInjectAction respond() {
+        response = new OpenInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public CloseInjectAction reject() {
+        response = new OpenInjectAction(driver);
+        driver.addScriptedElement(response);
+
+        CloseInjectAction closeAction = new CloseInjectAction(driver);
+        driver.addScriptedElement(closeAction);
+
+        return closeAction;
+    }
+
+    public CloseInjectAction reject(String condition, String description) {
+        return reject(Symbol.valueOf(condition), description);
+    }
+
+    public CloseInjectAction reject(String condition, String description, Map<String, Object> infoMap) {
+        return reject(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(infoMap));
+    }
+
+    public CloseInjectAction reject(Symbol condition, String description) {
+        return reject(condition, description, null);
+    }
+
+    public CloseInjectAction reject(Symbol condition, String description, Map<Symbol, Object> infoMap) {
+        response = new OpenInjectAction(driver);
+        driver.addScriptedElement(response);
+
+        CloseInjectAction closeAction = new CloseInjectAction(driver).withErrorCondition(condition, description, infoMap);
+        driver.addScriptedElement(closeAction);
+
+        return closeAction;
+    }
+
+    //----- Handle the performative and configure response is told to respond
+
+    @Override
+    public void handleOpen(Open open, ByteBuf payload, int channel, AMQPTestDriver context) {
+        super.handleOpen(open, payload, channel, context);
+
+        if (response != null) {
+            // Input was validated now populate response with auto values where not configured
+            // to say otherwise by the test.
+            if (response.getPerformative().getContainerId() == null && !explicitlyNullContainerId) {
+                response.getPerformative().setContainerId("driver");
+            }
+
+            if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+                response.onChannel(channel);
+            }
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public OpenExpectation withContainerId(String container) {
+        explicitlyNullContainerId = container == null;
+        return withContainerId(equalTo(container));
+    }
+
+    public OpenExpectation withHostname(String hostname) {
+        return withHostname(equalTo(hostname));
+    }
+
+    public OpenExpectation withMaxFrameSize(int maxFrameSize) {
+        return withMaxFrameSize(equalTo(UnsignedInteger.valueOf(maxFrameSize)));
+    }
+
+    public OpenExpectation withMaxFrameSize(long maxFrameSize) {
+        return withMaxFrameSize(equalTo(UnsignedInteger.valueOf(maxFrameSize)));
+    }
+
+    public OpenExpectation withMaxFrameSize(UnsignedInteger maxFrameSize) {
+        return withMaxFrameSize(equalTo(maxFrameSize));
+    }
+
+    public OpenExpectation withChannelMax(short channelMax) {
+        return withChannelMax(equalTo(UnsignedShort.valueOf(channelMax)));
+    }
+
+    public OpenExpectation withChannelMax(int channelMax) {
+        return withChannelMax(equalTo(UnsignedShort.valueOf(channelMax)));
+    }
+
+    public OpenExpectation withChannelMax(UnsignedShort channelMax) {
+        return withChannelMax(equalTo(channelMax));
+    }
+
+    public OpenExpectation withIdleTimeOut(int idleTimeout) {
+        return withIdleTimeOut(equalTo(UnsignedInteger.valueOf(idleTimeout)));
+    }
+
+    public OpenExpectation withIdleTimeOut(long idleTimeout) {
+        return withIdleTimeOut(equalTo(UnsignedInteger.valueOf(idleTimeout)));
+    }
+
+    public OpenExpectation withIdleTimeOut(UnsignedInteger idleTimeout) {
+        return withIdleTimeOut(equalTo(idleTimeout));
+    }
+
+    public OpenExpectation withOutgoingLocales(String... outgoingLocales) {
+        return withOutgoingLocales(equalTo(TypeMapper.toSymbolArray(outgoingLocales)));
+    }
+
+    public OpenExpectation withOutgoingLocales(Symbol... outgoingLocales) {
+        return withOutgoingLocales(equalTo(outgoingLocales));
+    }
+
+    public OpenExpectation withIncomingLocales(String... incomingLocales) {
+        return withIncomingLocales(equalTo(TypeMapper.toSymbolArray(incomingLocales)));
+    }
+
+    public OpenExpectation withIncomingLocales(Symbol... incomingLocales) {
+        return withIncomingLocales(equalTo(incomingLocales));
+    }
+
+    public OpenExpectation withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public OpenExpectation withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public OpenExpectation withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public OpenExpectation withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public OpenExpectation withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public OpenExpectation withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public OpenExpectation withContainerId(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.CONTAINER_ID, m);
+        return this;
+    }
+
+    public OpenExpectation withHostname(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.HOSTNAME, m);
+        return this;
+    }
+
+    public OpenExpectation withMaxFrameSize(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.MAX_FRAME_SIZE, m);
+        return this;
+    }
+
+    public OpenExpectation withChannelMax(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.CHANNEL_MAX, m);
+        return this;
+    }
+
+    public OpenExpectation withIdleTimeOut(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.IDLE_TIME_OUT, m);
+        return this;
+    }
+
+    public OpenExpectation withOutgoingLocales(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.OUTGOING_LOCALES, m);
+        return this;
+    }
+
+    public OpenExpectation withIncomingLocales(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.INCOMING_LOCALES, m);
+        return this;
+    }
+
+    public OpenExpectation withOfferedCapabilities(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.OFFERED_CAPABILITIES, m);
+        return this;
+    }
+
+    public OpenExpectation withDesiredCapabilities(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.DESIRED_CAPABILITIES, m);
+        return this;
+    }
+
+    public OpenExpectation withProperties(Matcher<?> m) {
+        matcher.addFieldMatcher(Open.Field.PROPERTIES, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<Open> getExpectedTypeClass() {
+        return Open.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslChallengeExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslChallengeExpectation.java
new file mode 100644
index 0000000..ef8815f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslChallengeExpectation.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.actions.SaslResponseInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslChallenge;
+import org.apache.qpid.protonj2.test.driver.matchers.security.SaslChallengeMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Scripted expectation for the AMQP SaslChallenge performative
+ */
+public class SaslChallengeExpectation extends AbstractExpectation<SaslChallenge> {
+
+    private final SaslChallengeMatcher matcher = new SaslChallengeMatcher();
+
+    public SaslChallengeExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    public SaslResponseInjectAction respond() {
+        SaslResponseInjectAction response = new SaslResponseInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslChallengeExpectation withChallenge(byte[] challenge) {
+        return withChallenge(equalTo(new Binary(challenge)));
+    }
+
+    public SaslChallengeExpectation withChallenge(Binary challenge) {
+        return withChallenge(equalTo(challenge));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslChallengeExpectation withChallenge(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslChallenge.Field.CHALLENGE, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<SaslChallenge> getExpectedTypeClass() {
+        return SaslChallenge.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslInitExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslInitExpectation.java
new file mode 100644
index 0000000..41422d8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslInitExpectation.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslInit;
+import org.apache.qpid.protonj2.test.driver.matchers.security.SaslInitMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Scripted expectation for the AMQP SaslInit performative
+ */
+public class SaslInitExpectation extends AbstractExpectation<SaslInit> {
+
+    private final SaslInitMatcher matcher = new SaslInitMatcher();
+
+    public SaslInitExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslInitExpectation withMechanism(String mechanism) {
+        return withMechanism(equalTo(Symbol.valueOf(mechanism)));
+    }
+
+    public SaslInitExpectation withMechanism(Symbol mechanism) {
+        return withMechanism(equalTo(mechanism));
+    }
+
+    public SaslInitExpectation withInitialResponse(byte[] initialResponse) {
+        return withInitialResponse(equalTo(new Binary(initialResponse)));
+    }
+
+    public SaslInitExpectation withInitialResponse(Binary initialResponse) {
+        return withInitialResponse(equalTo(initialResponse));
+    }
+
+    public SaslInitExpectation withHostname(String hostname) {
+        return withHostname(equalTo(hostname));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslInitExpectation withMechanism(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslInit.Field.MECHANISM, m);
+        return this;
+    }
+
+    public SaslInitExpectation withInitialResponse(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslInit.Field.INITIAL_RESPONSE, m);
+        return this;
+    }
+
+    public SaslInitExpectation withHostname(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslInit.Field.HOSTNAME, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<SaslInit> getExpectedTypeClass() {
+        return SaslInit.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslMechanismsExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslMechanismsExpectation.java
new file mode 100644
index 0000000..0065d9b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslMechanismsExpectation.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslMechanisms;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.security.SaslMechanismsMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Scripted expectation for the AMQP SaslMechanisms performative
+ */
+public class SaslMechanismsExpectation extends AbstractExpectation<SaslMechanisms> {
+
+    private final SaslMechanismsMatcher matcher = new SaslMechanismsMatcher();
+
+    public SaslMechanismsExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslMechanismsExpectation withSaslServerMechanisms(String... mechanisms) {
+        return withSaslServerMechanisms(equalTo(TypeMapper.toSymbolArray(mechanisms)));
+    }
+
+    public SaslMechanismsExpectation withSaslServerMechanisms(Symbol... mechanisms) {
+        return withSaslServerMechanisms(equalTo(mechanisms));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslMechanismsExpectation withSaslServerMechanisms(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslMechanisms.Field.SASL_SERVER_MECHANISMS, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<SaslMechanisms> getExpectedTypeClass() {
+        return SaslMechanisms.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslOutcomeExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslOutcomeExpectation.java
new file mode 100644
index 0000000..bf85b25
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslOutcomeExpectation.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslCode;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+import org.apache.qpid.protonj2.test.driver.matchers.security.SaslOutcomeMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Scripted expectation for the AMQP SaslOutcome performative
+ */
+public class SaslOutcomeExpectation extends AbstractExpectation<SaslOutcome> {
+
+    private final SaslOutcomeMatcher matcher = new SaslOutcomeMatcher();
+
+    public SaslOutcomeExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslOutcomeExpectation withCode(SaslCode code) {
+        return withCode(equalTo(code));
+    }
+
+    public SaslOutcomeExpectation withAdditionalData(byte[] additionalData) {
+        return withAdditionalData(equalTo(new Binary(additionalData)));
+    }
+
+    public SaslOutcomeExpectation withAdditionalData(Binary additionalData) {
+        return withAdditionalData(equalTo(additionalData));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslOutcomeExpectation withCode(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslOutcome.Field.CODE, m);
+        return this;
+    }
+
+    public SaslOutcomeExpectation withAdditionalData(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslOutcome.Field.ADDITIONAL_DATA, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<SaslOutcome> getExpectedTypeClass() {
+        return SaslOutcome.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslResponseExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslResponseExpectation.java
new file mode 100644
index 0000000..d137ed2
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/SaslResponseExpectation.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslResponse;
+import org.apache.qpid.protonj2.test.driver.matchers.security.SaslResponseMatcher;
+import org.hamcrest.Matcher;
+
+/**
+ * Scripted expectation for the AMQP SaslResponse performative
+ */
+public class SaslResponseExpectation extends AbstractExpectation<SaslResponse> {
+
+    private final SaslResponseMatcher matcher = new SaslResponseMatcher();
+
+    public SaslResponseExpectation(AMQPTestDriver driver) {
+        super(driver);
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslResponseExpectation withResponse(byte[] response) {
+        return withResponse(equalTo(new Binary(response)));
+    }
+
+    public SaslResponseExpectation withResponse(Binary response) {
+        return withResponse(equalTo(response));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslResponseExpectation withResponse(Matcher<?> m) {
+        matcher.addFieldMatcher(SaslResponse.Field.RESPONSE, m);
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Class<SaslResponse> getExpectedTypeClass() {
+        return SaslResponse.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/TransferExpectation.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/TransferExpectation.java
new file mode 100644
index 0000000..660fd9d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/expectations/TransferExpectation.java
@@ -0,0 +1,472 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.expectations;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import org.apache.qpid.protonj2.test.driver.AMQPTestDriver;
+import org.apache.qpid.protonj2.test.driver.LinkTracker;
+import org.apache.qpid.protonj2.test.driver.SessionTracker;
+import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
+import org.apache.qpid.protonj2.test.driver.actions.DispositionInjectAction;
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Rejected;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+import org.apache.qpid.protonj2.test.driver.matchers.transactions.TransactionalStateMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferMatcher;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Scripted expectation for the AMQP Transfer performative
+ */
+public class TransferExpectation extends AbstractExpectation<Transfer> {
+
+    private final TransferMatcher matcher = new TransferMatcher();
+    private final DeliveryStateBuilder stateBuilder = new DeliveryStateBuilder();
+
+    private Matcher<ByteBuf> payloadMatcher = Matchers.any(ByteBuf.class);
+
+    protected DispositionInjectAction response;
+
+    public TransferExpectation(AMQPTestDriver driver) {
+        super(driver);
+
+        // Default mandatory field validation.
+        withHandle(notNullValue());
+    }
+
+    public DispositionInjectAction respond() {
+        response = new DispositionInjectAction(driver);
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public DispositionInjectAction accept() {
+        response = new DispositionInjectAction(driver);
+        response.withSettled(true);
+        response.withState(Accepted.getInstance());
+
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public DispositionInjectAction release() {
+        response = new DispositionInjectAction(driver);
+        response.withSettled(true);
+        response.withState(Released.getInstance());
+
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public DispositionInjectAction reject() {
+        return reject(null);
+    }
+
+    public DispositionInjectAction reject(String condition, String description) {
+        return reject(new ErrorCondition(Symbol.valueOf(condition), description));
+    }
+
+    public DispositionInjectAction reject(Symbol condition, String description) {
+        return reject(new ErrorCondition(condition, description));
+    }
+
+    public DispositionInjectAction reject(ErrorCondition error) {
+        response = new DispositionInjectAction(driver);
+        response.withSettled(true);
+        response.withState(new Rejected().setError(error));
+
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    public DispositionInjectAction modify(boolean failed) {
+        return modify(failed, false);
+    }
+
+    public DispositionInjectAction modify(boolean failed, boolean undeliverable) {
+        response = new DispositionInjectAction(driver);
+        response.withSettled(true);
+        response.withState(new Modified().setDeliveryFailed(failed).setUndeliverableHere(undeliverable));
+
+        driver.addScriptedElement(response);
+        return response;
+    }
+
+    @Override
+    public TransferExpectation onChannel(int channel) {
+        super.onChannel(channel);
+        return this;
+    }
+
+    @Override
+    public void handleTransfer(Transfer transfer, ByteBuf payload, int channel, AMQPTestDriver driver) {
+        super.handleTransfer(transfer, payload, channel, driver);
+
+        final UnsignedShort remoteChannel = UnsignedShort.valueOf(channel);
+        final SessionTracker session = driver.sessions().getSessionFromRemoteChannel(remoteChannel);
+
+        if (session == null) {
+            throw new AssertionError(String.format(
+                "Received Detach on channel [%d] that has no matching Session for that remote channel. ", remoteChannel));
+        }
+
+        final LinkTracker link = session.handleTransfer(transfer, payload);
+
+        if (response != null) {
+            // Input was validated now populate response with auto values where not configured
+            // to say otherwise by the test.
+            if (response.onChannel() == BeginInjectAction.CHANNEL_UNSET) {
+                response.onChannel(link.getSession().getLocalChannel());
+            }
+
+            // Populate the fields of the response with defaults if non set by the test script
+            if (response.getPerformative().getFirst() == null) {
+                response.withFirst(transfer.getDeliveryId());
+            }
+
+            if (response.getPerformative().getRole() == null) {
+                response.withRole(link.getRole());
+            }
+
+            // Remaining response fields should be set by the test script as they can't be inferred.
+        }
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public TransferExpectation withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public TransferExpectation withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public TransferExpectation withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public TransferExpectation withDeliveryId(int deliveryId) {
+        return withDeliveryId(equalTo(UnsignedInteger.valueOf(deliveryId)));
+    }
+
+    public TransferExpectation withDeliveryId(long deliveryId) {
+        return withDeliveryId(equalTo(UnsignedInteger.valueOf(deliveryId)));
+    }
+
+    public TransferExpectation withDeliveryId(UnsignedInteger deliveryId) {
+        return withDeliveryId(equalTo(deliveryId));
+    }
+
+    public TransferExpectation withDeliveryTag(byte[] tag) {
+        return withDeliveryTag(new Binary(tag));
+    }
+
+    public TransferExpectation withDeliveryTag(Binary deliveryTag) {
+        return withDeliveryTag(equalTo(deliveryTag));
+    }
+
+    public TransferExpectation withMessageFormat(int messageFormat) {
+        return withMessageFormat(equalTo(UnsignedInteger.valueOf(messageFormat)));
+    }
+
+    public TransferExpectation withMessageFormat(long messageFormat) {
+        return withMessageFormat(equalTo(UnsignedInteger.valueOf(messageFormat)));
+    }
+
+    public TransferExpectation withMessageFormat(UnsignedInteger messageFormat) {
+        return withMessageFormat(equalTo(messageFormat));
+    }
+
+    public TransferExpectation withSettled(boolean settled) {
+        return withSettled(equalTo(settled));
+    }
+
+    public TransferExpectation withMore(boolean more) {
+        return withMore(equalTo(more));
+    }
+
+    public TransferExpectation withRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        return withRcvSettleMode(equalTo(rcvSettleMode.getValue()));
+    }
+
+    public TransferExpectation withState(DeliveryState state) {
+        return withState(equalTo(state));
+    }
+
+    public DeliveryStateBuilder withState() {
+        return stateBuilder;
+    }
+
+    public TransferExpectation withNullState() {
+        return withState(nullValue());
+    }
+
+    public TransferExpectation withResume(boolean resume) {
+        return withResume(equalTo(resume));
+    }
+
+    public TransferExpectation withAborted(boolean aborted) {
+        return withAborted(equalTo(aborted));
+    }
+
+    public TransferExpectation withBatchable(boolean batchable) {
+        return withBatchable(equalTo(batchable));
+    }
+
+    public TransferExpectation withNonNullPayload() {
+        this.payloadMatcher = notNullValue(ByteBuf.class);
+        return this;
+    }
+
+    public TransferExpectation withNullPayload() {
+        this.payloadMatcher = nullValue(ByteBuf.class);
+        return this;
+    }
+
+    public TransferExpectation withPayload(byte[] buffer) {
+        // TODO - Create Matcher which describes the mismatch in detail
+        this.payloadMatcher = Matchers.equalTo(Unpooled.wrappedBuffer(buffer));
+        return this;
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public TransferExpectation withHandle(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.HANDLE, m);
+        return this;
+    }
+
+    public TransferExpectation withDeliveryId(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.DELIVERY_ID, m);
+        return this;
+    }
+
+    public TransferExpectation withDeliveryTag(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.DELIVERY_TAG, m);
+        return this;
+    }
+
+    public TransferExpectation withMessageFormat(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.MESSAGE_FORMAT, m);
+        return this;
+    }
+
+    public TransferExpectation withSettled(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.SETTLED, m);
+        return this;
+    }
+
+    public TransferExpectation withMore(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.MORE, m);
+        return this;
+    }
+
+    public TransferExpectation withRcvSettleMode(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.RCV_SETTLE_MODE, m);
+        return this;
+    }
+
+    public TransferExpectation withState(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.STATE, m);
+        return this;
+    }
+
+    public TransferExpectation withResume(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.RESUME, m);
+        return this;
+    }
+
+    public TransferExpectation withAborted(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.ABORTED, m);
+        return this;
+    }
+
+    public TransferExpectation withBatchable(Matcher<?> m) {
+        matcher.addFieldMatcher(Transfer.Field.BATCHABLE, m);
+        return this;
+    }
+
+    public TransferExpectation withPayload(Matcher<ByteBuf> payloadMatcher) {
+        this.payloadMatcher = payloadMatcher;
+        return this;
+    }
+
+    @Override
+    protected Matcher<ListDescribedType> getExpectationMatcher() {
+        return matcher;
+    }
+
+    @Override
+    protected Matcher<ByteBuf> getPayloadMatcher() {
+        return payloadMatcher;
+    }
+
+    @Override
+    protected Class<Transfer> getExpectedTypeClass() {
+        return Transfer.class;
+    }
+
+    public final class DeliveryStateBuilder {
+
+        public TransferExpectation accepted() {
+            withState(Accepted.getInstance());
+            return TransferExpectation.this;
+        }
+
+        public TransferExpectation released() {
+            withState(Released.getInstance());
+            return TransferExpectation.this;
+        }
+
+        public TransferExpectation rejected() {
+            withState(new Rejected());
+            return TransferExpectation.this;
+        }
+
+        public TransferExpectation rejected(String condition, String description) {
+            withState(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return TransferExpectation.this;
+        }
+
+        public TransferExpectation modified() {
+            withState(new Modified());
+            return TransferExpectation.this;
+        }
+
+        public TransferExpectation modified(boolean failed) {
+            withState(new Modified());
+            return TransferExpectation.this;
+        }
+
+        public TransferExpectation modified(boolean failed, boolean undeliverableHere) {
+            withState(new Modified());
+            return TransferExpectation.this;
+        }
+
+        public TransferTransactionalStateMatcher transactional() {
+            TransferTransactionalStateMatcher matcher = new TransferTransactionalStateMatcher(TransferExpectation.this);
+            withState(matcher);
+            return matcher;
+        }
+    }
+
+    //----- Extend the TransactionalStateMatcher type to have an API suitable for Transfer expectation setup
+
+    public static class TransferTransactionalStateMatcher extends TransactionalStateMatcher {
+
+        private final TransferExpectation expectation;
+
+        public TransferTransactionalStateMatcher(TransferExpectation expectation) {
+            this.expectation = expectation;
+        }
+
+        public TransferExpectation also() {
+            return expectation;
+        }
+
+        public TransferExpectation and() {
+            return expectation;
+        }
+
+        @Override
+        public TransferTransactionalStateMatcher withTxnId(byte[] txnId) {
+            super.withTxnId(equalTo(new Binary(txnId)));
+            return this;
+        }
+
+        @Override
+        public TransferTransactionalStateMatcher withTxnId(Binary txnId) {
+            super.withTxnId(equalTo(txnId));
+            return this;
+        }
+
+        @Override
+        public TransferTransactionalStateMatcher withOutcome(DeliveryState outcome) {
+            super.withOutcome(equalTo(outcome));
+            return this;
+        }
+
+        //----- Matcher based with methods for more complex validation
+
+        @Override
+        public TransferTransactionalStateMatcher withTxnId(Matcher<?> m) {
+            super.withOutcome(m);
+            return this;
+        }
+
+        @Override
+        public TransferTransactionalStateMatcher withOutcome(Matcher<?> m) {
+            super.withOutcome(m);
+            return this;
+        }
+
+        // ----- Add a layer to allow configuring the outcome without specific type dependencies
+
+        public TransferTransactionalStateMatcher withAccepted() {
+            super.withOutcome(Accepted.getInstance());
+            return this;
+        }
+
+        public TransferTransactionalStateMatcher withReleased() {
+            super.withOutcome(Released.getInstance());
+            return this;
+        }
+
+        public TransferTransactionalStateMatcher withRejected() {
+            super.withOutcome(new Rejected());
+            return this;
+        }
+
+        public TransferTransactionalStateMatcher withRejected(String condition, String description) {
+            super.withOutcome(new Rejected().setError(new ErrorCondition(Symbol.valueOf(condition), description)));
+            return this;
+        }
+
+        public TransferTransactionalStateMatcher withModified() {
+            super.withOutcome(new Modified());
+            return this;
+        }
+
+        public TransferTransactionalStateMatcher withModified(boolean failed) {
+            super.withOutcome(new Modified().setDeliveryFailed(failed));
+            return this;
+        }
+
+        public TransferTransactionalStateMatcher withModified(boolean failed, boolean undeliverableHere) {
+            super.withOutcome(new Modified().setDeliveryFailed(failed).setUndeliverableHere(undeliverableHere));
+            return this;
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/ListDescribedTypeMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/ListDescribedTypeMatcher.java
new file mode 100644
index 0000000..68a7e58
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/ListDescribedTypeMatcher.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Matcher used to validate that received described types match expectations.
+ */
+public abstract class ListDescribedTypeMatcher extends TypeSafeMatcher<ListDescribedType> {
+
+    private String mismatchTextAddition;
+
+    private final int numFields;
+
+    private final UnsignedLong descriptorCode;
+    private final Symbol descriptorSymbol;
+
+    protected final Map<Enum<?>, Matcher<?>> fieldMatchers = new LinkedHashMap<>();
+
+    public ListDescribedTypeMatcher(int numFields, UnsignedLong code, Symbol symbol) {
+        this.descriptorCode = code;
+        this.descriptorSymbol = symbol;
+        this.numFields = numFields;
+    }
+
+    public ListDescribedTypeMatcher addFieldMatcher(Enum<?> field, Matcher<?> matcher) {
+        if (field.ordinal() > numFields) {
+            throw new IllegalArgumentException("Field enum supplied exceeds number of fields in type");
+        }
+
+        fieldMatchers.put(field, matcher);
+        return this;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText(getDescribedTypeClass().getSimpleName() + " which matches: ").appendValue(fieldMatchers);
+    }
+
+    @Override
+    protected boolean matchesSafely(ListDescribedType received) {
+        try {
+            Object descriptor = received.getDescriptor();
+            if (!descriptorCode.equals(descriptor) && !descriptorSymbol.equals(descriptor)) {
+                mismatchTextAddition = "Descriptor mismatch";
+                return false;
+            }
+
+            for (Map.Entry<Enum<?>, Matcher<?>> entry : fieldMatchers.entrySet()) {
+                @SuppressWarnings("unchecked")
+                Matcher<Object> matcher = (Matcher<Object>) entry.getValue();
+                assertThat("Field " + entry.getKey() + " value should match",
+                    received.getFieldValue(entry.getKey().ordinal()), matcher);
+            }
+        } catch (AssertionError ae) {
+            mismatchTextAddition = "AssertionFailure: " + ae.getMessage();
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    protected void describeMismatchSafely(ListDescribedType item, Description mismatchDescription) {
+        mismatchDescription.appendText("\nActual form: ").appendValue(item);
+
+        mismatchDescription.appendText("\nExpected descriptor: ")
+                           .appendValue(descriptorSymbol)
+                           .appendText(" / ")
+                           .appendValue(descriptorCode);
+
+        if (mismatchTextAddition != null) {
+            mismatchDescription.appendText("\nAdditional info: ").appendValue(mismatchTextAddition);
+        }
+    }
+
+    protected abstract Class<?> getDescribedTypeClass();
+
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractListSectionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractListSectionMatcher.java
new file mode 100644
index 0000000..7fc9361
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractListSectionMatcher.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+public abstract class AbstractListSectionMatcher extends AbstractMessageSectionMatcher {
+
+    public AbstractListSectionMatcher(UnsignedLong numericDescriptor, Symbol symbolicDescriptor, Map<Object, Matcher<?>> fieldMatchers, boolean expectTrailingBytes) {
+        super(numericDescriptor, symbolicDescriptor, fieldMatchers, expectTrailingBytes);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void verifyReceivedDescribedObject(Object described) {
+        if (!(described instanceof List)) {
+            throw new IllegalArgumentException(
+                "Unexpected section contents. Expected List, but got: " + (described == null ? "null" : described.getClass()));
+        }
+
+        int fieldNumber = 0;
+        Map<Object, Object> valueMap = new HashMap<>();
+        for (Object value : (List<Object>) described) {
+            valueMap.put(getField(fieldNumber++), value);
+        }
+
+        verifyReceivedFields(valueMap);
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractMapSectionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractMapSectionMatcher.java
new file mode 100644
index 0000000..3ae4ba2
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractMapSectionMatcher.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+public abstract class AbstractMapSectionMatcher extends AbstractMessageSectionMatcher {
+
+    public AbstractMapSectionMatcher(UnsignedLong numericDescriptor, Symbol symbolicDescriptor, Map<Object, Matcher<?>> fieldMatchers, boolean expectTrailingBytes) {
+        super(numericDescriptor, symbolicDescriptor, fieldMatchers, expectTrailingBytes);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void verifyReceivedDescribedObject(Object described) {
+        if (!(described instanceof Map)) {
+            throw new IllegalArgumentException(
+                "Unexpected section contents. Expected Map, but got: " + (described == null ? "null" : described.getClass()));
+        }
+
+        verifyReceivedFields((Map<Object, Object>) described);
+    }
+
+    public AbstractMapSectionMatcher withEntry(Object key, Matcher<?> m) {
+        getMatchers().put(key, m);
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractMessageSectionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractMessageSectionMatcher.java
new file mode 100644
index 0000000..bf36fb6
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AbstractMessageSectionMatcher.java
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.Codec;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.buffer.ByteBuf;
+
+public abstract class AbstractMessageSectionMatcher {
+
+    private final Logger LOG = LoggerFactory.getLogger(getClass());
+
+    private final UnsignedLong numericDescriptor;
+    private final Symbol symbolicDescriptor;
+
+    private final Map<Object, Matcher<?>> fieldMatchers;
+    private Map<Object, Object> receivedFields;
+
+    private final boolean expectTrailingBytes;
+
+    protected AbstractMessageSectionMatcher(UnsignedLong numericDescriptor, Symbol symbolicDescriptor, Map<Object, Matcher<?>> fieldMatchers, boolean expectTrailingBytes) {
+        this.numericDescriptor = numericDescriptor;
+        this.symbolicDescriptor = symbolicDescriptor;
+        this.fieldMatchers = fieldMatchers;
+        this.expectTrailingBytes = expectTrailingBytes;
+    }
+
+    protected Map<Object, Matcher<?>> getMatchers() {
+        return fieldMatchers;
+    }
+
+    protected Map<Object, Object> getReceivedFields() {
+        return receivedFields;
+    }
+
+    /**
+     * @param receivedBinary
+     *        The received Binary value that should be validated.
+     *
+     * @return the number of bytes consumed from the provided Binary
+     *
+     * @throws RuntimeException
+     *         if the provided Binary does not match expectation in some way
+     */
+    public int verify(ByteBuf receivedBinary) throws RuntimeException {
+        int length = receivedBinary.readableBytes();
+        Codec data = Codec.Factory.create();
+        long decoded = data.decode(receivedBinary);
+        if (decoded > Integer.MAX_VALUE) {
+            throw new IllegalStateException("Decoded more bytes than Binary supports holding");
+        }
+
+        if (decoded < length && !expectTrailingBytes) {
+            throw new IllegalArgumentException(
+                "Expected to consume all bytes, but trailing bytes remain: Got " + length + ", consumed " + decoded);
+        }
+
+        DescribedType decodedDescribedType = data.getDescribedType();
+        verifyReceivedDescribedType(decodedDescribedType);
+
+        // Need to cast to int, but verified earlier that it is <
+        // Integer.MAX_VALUE
+        return (int) decoded;
+    }
+
+    private void verifyReceivedDescribedType(DescribedType decodedDescribedType) {
+        Object descriptor = decodedDescribedType.getDescriptor();
+        if (!(symbolicDescriptor.equals(descriptor) || numericDescriptor.equals(descriptor))) {
+            throw new IllegalArgumentException(
+                "Unexpected section type descriptor. Expected " + symbolicDescriptor +
+                " or " + numericDescriptor + ", but got: " + descriptor);
+        }
+
+        verifyReceivedDescribedObject(decodedDescribedType.getDescribed());
+    }
+
+    /**
+     * sub-classes should implement depending on the expected content of the
+     * particular section type.
+     */
+    protected abstract void verifyReceivedDescribedObject(Object describedObject);
+
+    /**
+     * Utility method for use by sub-classes that expect field-based sections,
+     * i.e lists or maps.
+     */
+    protected void verifyReceivedFields(Map<Object, Object> valueMap) {
+        receivedFields = valueMap;
+
+        LOG.debug("About to check the fields of the section." + "\n  Received:" + valueMap + "\n  Expectations: " + fieldMatchers);
+        for (Map.Entry<Object, Matcher<?>> entry : fieldMatchers.entrySet()) {
+            @SuppressWarnings("unchecked")
+            Matcher<Object> matcher = (Matcher<Object>) entry.getValue();
+            Object field = entry.getKey();
+            assertThat("Field " + field + " value should match", valueMap.get(field), matcher);
+        }
+    }
+
+    /**
+     * Intended to be overridden in most cases that use the above method (but
+     * not necessarily all - hence not marked as abstract)
+     */
+    protected Enum<?> getField(int fieldIndex) {
+        throw new UnsupportedOperationException("getFieldName is expected to be overridden by subclass if it is required");
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AcceptedMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AcceptedMatcher.java
new file mode 100644
index 0000000..e3a06c0
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/AcceptedMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class AcceptedMatcher extends ListDescribedTypeMatcher {
+
+    public AcceptedMatcher() {
+        super(0, Accepted.DESCRIPTOR_CODE, Accepted.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Accepted.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ApplicationPropertiesMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ApplicationPropertiesMatcher.java
new file mode 100644
index 0000000..23b4ea8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ApplicationPropertiesMatcher.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import java.util.HashMap;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+public class ApplicationPropertiesMatcher extends AbstractMapSectionMatcher {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:application-properties:map");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000074L);
+
+    public ApplicationPropertiesMatcher(boolean expectTrailingBytes) {
+        super(DESCRIPTOR_CODE, DESCRIPTOR_SYMBOL, new HashMap<Object, Matcher<?>>(), expectTrailingBytes);
+    }
+
+    @Override
+    public ApplicationPropertiesMatcher withEntry(Object key, Matcher<?> m) {
+        if (!(key instanceof String)) {
+            throw new RuntimeException("ApplicationProperties maps must use non-null String keys");
+        }
+
+        return (ApplicationPropertiesMatcher) super.withEntry(key, m);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnCloseMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnCloseMatcher.java
new file mode 100644
index 0000000..782b26b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnCloseMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnClose;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class DeleteOnCloseMatcher extends ListDescribedTypeMatcher {
+
+    public DeleteOnCloseMatcher() {
+        super(0, DeleteOnClose.DESCRIPTOR_CODE, DeleteOnClose.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return DeleteOnClose.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoLinksMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoLinksMatcher.java
new file mode 100644
index 0000000..064a3dc
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoLinksMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnNoLinks;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class DeleteOnNoLinksMatcher extends ListDescribedTypeMatcher {
+
+    public DeleteOnNoLinksMatcher() {
+        super(0, DeleteOnNoLinks.DESCRIPTOR_CODE, DeleteOnNoLinks.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return DeleteOnNoLinks.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoLinksOrMessagesMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoLinksOrMessagesMatcher.java
new file mode 100644
index 0000000..d14622e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoLinksOrMessagesMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnNoLinksOrMessages;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class DeleteOnNoLinksOrMessagesMatcher extends ListDescribedTypeMatcher {
+
+    public DeleteOnNoLinksOrMessagesMatcher() {
+        super(0, DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE, DeleteOnNoLinksOrMessages.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return DeleteOnNoLinksOrMessages.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoMessagesMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoMessagesMatcher.java
new file mode 100644
index 0000000..8d86663
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeleteOnNoMessagesMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.DeleteOnNoMessages;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class DeleteOnNoMessagesMatcher extends ListDescribedTypeMatcher {
+
+    public DeleteOnNoMessagesMatcher() {
+        super(0, DeleteOnNoMessages.DESCRIPTOR_CODE, DeleteOnNoMessages.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return DeleteOnNoMessages.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeliveryAnnotationsMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeliveryAnnotationsMatcher.java
new file mode 100644
index 0000000..990f011
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/DeliveryAnnotationsMatcher.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+public class DeliveryAnnotationsMatcher extends AbstractMapSectionMatcher {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delivery-annotations:map");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000071L);
+
+    public DeliveryAnnotationsMatcher(boolean expectTrailingBytes) {
+        super(DESCRIPTOR_CODE, DESCRIPTOR_SYMBOL, new HashMap<Object, Matcher<?>>(), expectTrailingBytes);
+    }
+
+    @Override
+    public DeliveryAnnotationsMatcher withEntry(Object key, Matcher<?> m) {
+        validateType(key);
+
+        return (DeliveryAnnotationsMatcher) super.withEntry(key, m);
+    }
+
+    private void validateType(Object key) {
+        if (!(key instanceof Long || key instanceof Symbol)) {
+            throw new IllegalArgumentException("Delivery Annotation keys must be of type Symbol or long (reserved)");
+        }
+    }
+
+    public DeliveryAnnotationsMatcher withEntry(String key, Matcher<?> m) {
+        getMatchers().put(Symbol.valueOf(key), m);
+        return this;
+    }
+
+    public boolean keyExistsInReceivedAnnotations(Object key) {
+        validateType(key);
+
+        Map<Object, Object> receivedFields = super.getReceivedFields();
+
+        if (receivedFields != null) {
+            return receivedFields.containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    public Object getReceivedAnnotation(Symbol key) {
+        Map<Object, Object> receivedFields = super.getReceivedFields();
+
+        return receivedFields.get(key);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/FooterMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/FooterMatcher.java
new file mode 100644
index 0000000..8cecc69
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/FooterMatcher.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+public class FooterMatcher extends AbstractMapSectionMatcher {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:footer:map");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000078L);
+
+    public FooterMatcher(boolean expectTrailingBytes) {
+        super(DESCRIPTOR_CODE, DESCRIPTOR_SYMBOL, new HashMap<Object, Matcher<?>>(), expectTrailingBytes);
+    }
+
+    @Override
+    public FooterMatcher withEntry(Object key, Matcher<?> m) {
+        validateType(key);
+
+        return (FooterMatcher) super.withEntry(key, m);
+    }
+
+    private void validateType(Object key) {
+        if (!(key instanceof Long || key instanceof Symbol)) {
+            throw new IllegalArgumentException("Footer keys must be of type Symbol or long (reserved)");
+        }
+    }
+
+    public FooterMatcher withEntry(String key, Matcher<?> m) {
+        getMatchers().put(Symbol.valueOf(key), m);
+        return this;
+    }
+
+    public boolean keyExistsInReceivedAnnotations(Object key) {
+        validateType(key);
+
+        Map<Object, Object> receivedFields = super.getReceivedFields();
+
+        if (receivedFields != null) {
+            return receivedFields.containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    public Object getReceivedAnnotation(Symbol key) {
+        Map<Object, Object> receivedFields = super.getReceivedFields();
+
+        return receivedFields.get(key);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/HeaderMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/HeaderMatcher.java
new file mode 100644
index 0000000..699165e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/HeaderMatcher.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.HashMap;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+/**
+ * Generated by generate-message-section-matchers.xsl, which resides in this
+ * package.
+ */
+public class HeaderMatcher extends AbstractListSectionMatcher {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:header:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000070L);
+
+    /**
+     * Note that the ordinal of the Field enumeration match the order specified in
+     * the AMQP specification
+     */
+    public enum Field {
+        DURABLE,
+        PRIORITY,
+        TTL,
+        FIRST_ACQUIRER,
+        DELIVERY_COUNT
+    }
+
+    public HeaderMatcher(boolean expectTrailingBytes) {
+        super(DESCRIPTOR_CODE, DESCRIPTOR_SYMBOL, new HashMap<Object, Matcher<?>>(), expectTrailingBytes);
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public HeaderMatcher withDurable(boolean durable) {
+        return withDurable(equalTo(durable));
+    }
+
+    public HeaderMatcher withDurable(Boolean durable) {
+        return withDurable(equalTo(durable));
+    }
+
+    public HeaderMatcher withPriority(byte priority) {
+        return withPriority(equalTo(UnsignedByte.valueOf(priority)));
+    }
+
+    public HeaderMatcher withPriority(UnsignedByte priority) {
+        return withPriority(equalTo(priority));
+    }
+
+    public HeaderMatcher withTtl(int timeToLive) {
+        return withTtl(equalTo(UnsignedInteger.valueOf(timeToLive)));
+    }
+
+    public HeaderMatcher withTtl(long timeToLive) {
+        return withTtl(equalTo(UnsignedInteger.valueOf(timeToLive)));
+    }
+
+    public HeaderMatcher withTtl(UnsignedInteger timeToLive) {
+        return withTtl(equalTo(timeToLive));
+    }
+
+    public HeaderMatcher withFirstAcquirer(boolean durable) {
+        return withFirstAcquirer(equalTo(durable));
+    }
+
+    public HeaderMatcher withFirstAcquirer(Boolean durable) {
+        return withFirstAcquirer(equalTo(durable));
+    }
+
+    public HeaderMatcher withDeliveryCount(int deliveryCount) {
+        return withDeliveryCount(equalTo(UnsignedInteger.valueOf(deliveryCount)));
+    }
+
+    public HeaderMatcher withDeliveryCount(long deliveryCount) {
+        return withDeliveryCount(equalTo(UnsignedInteger.valueOf(deliveryCount)));
+    }
+
+    public HeaderMatcher withDeliveryCount(UnsignedInteger deliveryCount) {
+        return withDeliveryCount(equalTo(deliveryCount));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public HeaderMatcher withDurable(Matcher<?> m) {
+        getMatchers().put(Field.DURABLE, m);
+        return this;
+    }
+
+    public HeaderMatcher withPriority(Matcher<?> m) {
+        getMatchers().put(Field.PRIORITY, m);
+        return this;
+    }
+
+    public HeaderMatcher withTtl(Matcher<?> m) {
+        getMatchers().put(Field.TTL, m);
+        return this;
+    }
+
+    public HeaderMatcher withFirstAcquirer(Matcher<?> m) {
+        getMatchers().put(Field.FIRST_ACQUIRER, m);
+        return this;
+    }
+
+    public HeaderMatcher withDeliveryCount(Matcher<?> m) {
+        getMatchers().put(Field.DELIVERY_COUNT, m);
+        return this;
+    }
+
+    public Object getReceivedDurable() {
+        return getReceivedFields().get(Field.DURABLE);
+    }
+
+    public Object getReceivedPriority() {
+        return getReceivedFields().get(Field.PRIORITY);
+    }
+
+    public Object getReceivedTtl() {
+        return getReceivedFields().get(Field.TTL);
+    }
+
+    public Object getReceivedFirstAcquirer() {
+        return getReceivedFields().get(Field.FIRST_ACQUIRER);
+    }
+
+    public Object getReceivedDeliveryCount() {
+        return getReceivedFields().get(Field.DELIVERY_COUNT);
+    }
+
+    @Override
+    protected Enum<?> getField(int fieldIndex) {
+        return Field.values()[fieldIndex];
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/MessageAnnotationsMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/MessageAnnotationsMatcher.java
new file mode 100644
index 0000000..15c44e8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/MessageAnnotationsMatcher.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+public class MessageAnnotationsMatcher extends AbstractMapSectionMatcher {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:message-annotations:map");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000072L);
+
+    public MessageAnnotationsMatcher(boolean expectTrailingBytes) {
+        super(DESCRIPTOR_CODE, DESCRIPTOR_SYMBOL, new HashMap<Object, Matcher<?>>(), expectTrailingBytes);
+    }
+
+    @Override
+    public MessageAnnotationsMatcher withEntry(Object key, Matcher<?> m) {
+        validateType(key);
+
+        return (MessageAnnotationsMatcher) super.withEntry(key, m);
+    }
+
+    private void validateType(Object key) {
+        if (!(key instanceof Long || key instanceof Symbol)) {
+            throw new IllegalArgumentException("Message Annotation keys must be of type Symbol or long (reserved)");
+        }
+    }
+
+    public MessageAnnotationsMatcher withEntry(String key, Matcher<?> m) {
+        getMatchers().put(Symbol.valueOf(key), m);
+        return this;
+    }
+
+    public boolean keyExistsInReceivedAnnotations(Object key) {
+        validateType(key);
+
+        Map<Object, Object> receivedFields = super.getReceivedFields();
+
+        if (receivedFields != null) {
+            return receivedFields.containsKey(key);
+        } else {
+            return false;
+        }
+    }
+
+    public Object getReceivedAnnotation(Symbol key) {
+        Map<Object, Object> receivedFields = super.getReceivedFields();
+
+        return receivedFields.get(key);
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ModifiedMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ModifiedMatcher.java
new file mode 100644
index 0000000..82ac62f
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ModifiedMatcher.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Modified;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class ModifiedMatcher extends ListDescribedTypeMatcher {
+
+    public ModifiedMatcher() {
+        super(Modified.Field.values().length, Modified.DESCRIPTOR_CODE, Modified.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Modified.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public ModifiedMatcher withDeliveryFailed(boolean deliveryFailed) {
+        return withDeliveryFailed(equalTo(deliveryFailed));
+    }
+
+    public ModifiedMatcher withDeliveryFailed(Boolean deliveryFailed) {
+        return withDeliveryFailed(equalTo(deliveryFailed));
+    }
+
+    public ModifiedMatcher withUndeliverableHere(boolean undeliverableHere) {
+        return withUndeliverableHere(equalTo(undeliverableHere));
+    }
+
+    public ModifiedMatcher withUndeliverableHere(Boolean undeliverableHere) {
+        return withUndeliverableHere(equalTo(undeliverableHere));
+    }
+
+    public ModifiedMatcher withMessageAnnotationsMap(Map<Symbol, Object> sectionNo) {
+        return withMessageAnnotations(equalTo(sectionNo));
+    }
+
+    public ModifiedMatcher withMessageAnnotations(Map<String, Object> sectionNo) {
+        return withMessageAnnotations(equalTo(TypeMapper.toSymbolKeyedMap(sectionNo)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public ModifiedMatcher withDeliveryFailed(Matcher<?> m) {
+        addFieldMatcher(Modified.Field.DELIVERY_FAILED, m);
+        return this;
+    }
+
+    public ModifiedMatcher withUndeliverableHere(Matcher<?> m) {
+        addFieldMatcher(Modified.Field.UNDELIVERABLE_HERE, m);
+        return this;
+    }
+
+    public ModifiedMatcher withMessageAnnotations(Matcher<?> m) {
+        addFieldMatcher(Modified.Field.MESSAGE_ANNOTATIONS, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/OpenMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/OpenMatcher.java
new file mode 100644
index 0000000..7f18174
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/OpenMatcher.java
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class OpenMatcher extends ListDescribedTypeMatcher {
+
+    public OpenMatcher() {
+        super(Open.Field.values().length, Open.DESCRIPTOR_CODE, Open.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Open.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public OpenMatcher withContainerId(String container) {
+        return withContainerId(equalTo(container));
+    }
+
+    public OpenMatcher withHostname(String hostname) {
+        return withHostname(equalTo(hostname));
+    }
+
+    public OpenMatcher withMaxFrameSize(int maxFrameSize) {
+        return withMaxFrameSize(equalTo(UnsignedInteger.valueOf(maxFrameSize)));
+    }
+
+    public OpenMatcher withMaxFrameSize(long maxFrameSize) {
+        return withMaxFrameSize(equalTo(UnsignedInteger.valueOf(maxFrameSize)));
+    }
+
+    public OpenMatcher withMaxFrameSize(UnsignedInteger maxFrameSize) {
+        return withMaxFrameSize(equalTo(maxFrameSize));
+    }
+
+    public OpenMatcher withChannelMax(short channelMax) {
+        return withChannelMax(equalTo(UnsignedShort.valueOf(channelMax)));
+    }
+
+    public OpenMatcher withChannelMax(int channelMax) {
+        return withChannelMax(equalTo(UnsignedShort.valueOf(channelMax)));
+    }
+
+    public OpenMatcher withChannelMax(UnsignedShort channelMax) {
+        return withChannelMax(equalTo(channelMax));
+    }
+
+    public OpenMatcher withIdleTimeOut(int idleTimeout) {
+        return withIdleTimeOut(equalTo(UnsignedInteger.valueOf(idleTimeout)));
+    }
+
+    public OpenMatcher withIdleTimeOut(long idleTimeout) {
+        return withIdleTimeOut(equalTo(UnsignedInteger.valueOf(idleTimeout)));
+    }
+
+    public OpenMatcher withIdleTimeOut(UnsignedInteger idleTimeout) {
+        return withIdleTimeOut(equalTo(idleTimeout));
+    }
+
+    public OpenMatcher withOutgoingLocales(String... outgoingLocales) {
+        return withOutgoingLocales(equalTo(TypeMapper.toSymbolArray(outgoingLocales)));
+    }
+
+    public OpenMatcher withOutgoingLocales(Symbol... outgoingLocales) {
+        return withOutgoingLocales(equalTo(outgoingLocales));
+    }
+
+    public OpenMatcher withIncomingLocales(String... incomingLocales) {
+        return withIncomingLocales(equalTo(TypeMapper.toSymbolArray(incomingLocales)));
+    }
+
+    public OpenMatcher withIncomingLocales(Symbol... incomingLocales) {
+        return withIncomingLocales(equalTo(incomingLocales));
+    }
+
+    public OpenMatcher withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public OpenMatcher withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public OpenMatcher withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public OpenMatcher withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public OpenMatcher withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public OpenMatcher withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public OpenMatcher withContainerId(Matcher<?> m) {
+        addFieldMatcher(Open.Field.CONTAINER_ID, m);
+        return this;
+    }
+
+    public OpenMatcher withHostname(Matcher<?> m) {
+        addFieldMatcher(Open.Field.HOSTNAME, m);
+        return this;
+    }
+
+    public OpenMatcher withMaxFrameSize(Matcher<?> m) {
+        addFieldMatcher(Open.Field.MAX_FRAME_SIZE, m);
+        return this;
+    }
+
+    public OpenMatcher withChannelMax(Matcher<?> m) {
+        addFieldMatcher(Open.Field.CHANNEL_MAX, m);
+        return this;
+    }
+
+    public OpenMatcher withIdleTimeOut(Matcher<?> m) {
+        addFieldMatcher(Open.Field.IDLE_TIME_OUT, m);
+        return this;
+    }
+
+    public OpenMatcher withOutgoingLocales(Matcher<?> m) {
+        addFieldMatcher(Open.Field.OUTGOING_LOCALES, m);
+        return this;
+    }
+
+    public OpenMatcher withIncomingLocales(Matcher<?> m) {
+        addFieldMatcher(Open.Field.INCOMING_LOCALES, m);
+        return this;
+    }
+
+    public OpenMatcher withOfferedCapabilities(Matcher<?> m) {
+        addFieldMatcher(Open.Field.OFFERED_CAPABILITIES, m);
+        return this;
+    }
+
+    public OpenMatcher withDesiredCapabilities(Matcher<?> m) {
+        addFieldMatcher(Open.Field.DESIRED_CAPABILITIES, m);
+        return this;
+    }
+
+    public OpenMatcher withProperties(Matcher<?> m) {
+        addFieldMatcher(Open.Field.PROPERTIES, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/PropertiesMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/PropertiesMatcher.java
new file mode 100644
index 0000000..37f87f1
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/PropertiesMatcher.java
@@ -0,0 +1,291 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Date;
+import java.util.HashMap;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Matcher;
+
+/**
+ * Generated by generate-message-section-matchers.xsl, which resides in this
+ * package.
+ */
+public class PropertiesMatcher extends AbstractListSectionMatcher {
+
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:properties:list");
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000073L);
+
+    /**
+     * Note that the ordinal of the Field enumeration match the order specified in
+     * the AMQP specification
+     */
+    public enum Field {
+        MESSAGE_ID,
+        USER_ID,
+        TO,
+        SUBJECT,
+        REPLY_TO,
+        CORRELATION_ID,
+        CONTENT_TYPE,
+        CONTENT_ENCODING,
+        ABSOLUTE_EXPIRY_TIME,
+        CREATION_TIME,
+        GROUP_ID,
+        GROUP_SEQUENCE,
+        REPLY_TO_GROUP_ID,
+    }
+
+    public PropertiesMatcher(boolean expectTrailingBytes) {
+        super(DESCRIPTOR_CODE, DESCRIPTOR_SYMBOL, new HashMap<Object, Matcher<?>>(), expectTrailingBytes);
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public PropertiesMatcher withMessageId(Object messageId) {
+        return withMessageId(equalTo(messageId));
+    }
+
+    public PropertiesMatcher withUserId(byte[] userId) {
+        return withUserId(equalTo(new Binary(userId)));
+    }
+
+    public PropertiesMatcher withUserId(Binary userId) {
+        return withUserId(equalTo(userId));
+    }
+
+    public PropertiesMatcher withTo(String to) {
+        return withTo(equalTo(to));
+    }
+
+    public PropertiesMatcher withSubject(String subject) {
+        return withSubject(equalTo(subject));
+    }
+
+    public PropertiesMatcher withReplyTo(String replyTo) {
+        return withReplyTo(equalTo(replyTo));
+    }
+
+    public PropertiesMatcher withCorrelationId(Object correlationId) {
+        return withCorrelationId(equalTo(correlationId));
+    }
+
+    public PropertiesMatcher withContentType(String contentType) {
+        return withContentType(equalTo(Symbol.valueOf(contentType)));
+    }
+
+    public PropertiesMatcher withContentType(Symbol contentType) {
+        return withContentType(equalTo(contentType));
+    }
+
+    public PropertiesMatcher withContentEncoding(String contentEncoding) {
+        return withContentEncoding(equalTo(Symbol.valueOf(contentEncoding)));
+    }
+
+    public PropertiesMatcher withContentEncoding(Symbol contentEncoding) {
+        return withContentEncoding(equalTo(contentEncoding));
+    }
+
+    public PropertiesMatcher withAbsoluteExpiryTime(int absoluteExpiryTime) {
+        return withAbsoluteExpiryTime(equalTo(new Date(absoluteExpiryTime)));
+    }
+
+    public PropertiesMatcher withAbsoluteExpiryTime(long absoluteExpiryTime) {
+        return withAbsoluteExpiryTime(equalTo(new Date(absoluteExpiryTime)));
+    }
+
+    public PropertiesMatcher withAbsoluteExpiryTime(Long absoluteExpiryTime) {
+        if (absoluteExpiryTime == null) {
+            return withAbsoluteExpiryTime(nullValue());
+        } else {
+            return withAbsoluteExpiryTime(equalTo(new Date(absoluteExpiryTime)));
+        }
+    }
+
+    public PropertiesMatcher withCreationTime(int creationTime) {
+        return withCreationTime(equalTo(new Date(creationTime)));
+    }
+
+    public PropertiesMatcher withCreationTime(long creationTime) {
+        return withCreationTime(equalTo(new Date(creationTime)));
+    }
+
+    public PropertiesMatcher withCreationTime(Long creationTime) {
+        if (creationTime == null) {
+            return withCreationTime(nullValue());
+        } else {
+            return withCreationTime(equalTo(new Date(creationTime)));
+        }
+    }
+
+    public PropertiesMatcher withGroupId(String groupId) {
+        return withGroupId(equalTo(groupId));
+    }
+
+    public PropertiesMatcher withGroupSequence(int groupSequence) {
+        return withGroupSequence(equalTo(UnsignedInteger.valueOf(groupSequence)));
+    }
+
+    public PropertiesMatcher withGroupSequence(long groupSequence) {
+        return withGroupSequence(equalTo(UnsignedInteger.valueOf(groupSequence)));
+    }
+
+    public PropertiesMatcher withGroupSequence(Long groupSequence) {
+        if (groupSequence == null) {
+            return withGroupSequence(nullValue());
+        } else {
+            return withGroupSequence(equalTo(UnsignedInteger.valueOf(groupSequence.longValue())));
+        }
+    }
+
+    public PropertiesMatcher withReplyToGroupId(String replyToGroupId) {
+        return withReplyToGroupId(equalTo(replyToGroupId));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public PropertiesMatcher withMessageId(Matcher<?> m) {
+        getMatchers().put(Field.MESSAGE_ID, m);
+        return this;
+    }
+
+    public PropertiesMatcher withUserId(Matcher<?> m) {
+        getMatchers().put(Field.USER_ID, m);
+        return this;
+    }
+
+    public PropertiesMatcher withTo(Matcher<?> m) {
+        getMatchers().put(Field.TO, m);
+        return this;
+    }
+
+    public PropertiesMatcher withSubject(Matcher<?> m) {
+        getMatchers().put(Field.SUBJECT, m);
+        return this;
+    }
+
+    public PropertiesMatcher withReplyTo(Matcher<?> m) {
+        getMatchers().put(Field.REPLY_TO, m);
+        return this;
+    }
+
+    public PropertiesMatcher withCorrelationId(Matcher<?> m) {
+        getMatchers().put(Field.CORRELATION_ID, m);
+        return this;
+    }
+
+    public PropertiesMatcher withContentType(Matcher<?> m) {
+        getMatchers().put(Field.CONTENT_TYPE, m);
+        return this;
+    }
+
+    public PropertiesMatcher withContentEncoding(Matcher<?> m) {
+        getMatchers().put(Field.CONTENT_ENCODING, m);
+        return this;
+    }
+
+    public PropertiesMatcher withAbsoluteExpiryTime(Matcher<?> m) {
+        getMatchers().put(Field.ABSOLUTE_EXPIRY_TIME, m);
+        return this;
+    }
+
+    public PropertiesMatcher withCreationTime(Matcher<?> m) {
+        getMatchers().put(Field.CREATION_TIME, m);
+        return this;
+    }
+
+    public PropertiesMatcher withGroupId(Matcher<?> m) {
+        getMatchers().put(Field.GROUP_ID, m);
+        return this;
+    }
+
+    public PropertiesMatcher withGroupSequence(Matcher<?> m) {
+        getMatchers().put(Field.GROUP_SEQUENCE, m);
+        return this;
+    }
+
+    public PropertiesMatcher withReplyToGroupId(Matcher<?> m) {
+        getMatchers().put(Field.REPLY_TO_GROUP_ID, m);
+        return this;
+    }
+
+    public Object getReceivedMessageId() {
+        return getReceivedFields().get(Field.MESSAGE_ID);
+    }
+
+    public Object getReceivedUserId() {
+        return getReceivedFields().get(Field.USER_ID);
+    }
+
+    public Object getReceivedTo() {
+        return getReceivedFields().get(Field.TO);
+    }
+
+    public Object getReceivedSubject() {
+        return getReceivedFields().get(Field.SUBJECT);
+    }
+
+    public Object getReceivedReplyTo() {
+        return getReceivedFields().get(Field.REPLY_TO);
+    }
+
+    public Object getReceivedCorrelationId() {
+        return getReceivedFields().get(Field.CORRELATION_ID);
+    }
+
+    public Object getReceivedContentType() {
+        return getReceivedFields().get(Field.CONTENT_TYPE);
+    }
+
+    public Object getReceivedContentEncoding() {
+        return getReceivedFields().get(Field.CONTENT_ENCODING);
+    }
+
+    public Object getReceivedAbsoluteExpiryTime() {
+        return getReceivedFields().get(Field.ABSOLUTE_EXPIRY_TIME);
+    }
+
+    public Object getReceivedCreationTime() {
+        return getReceivedFields().get(Field.CREATION_TIME);
+    }
+
+    public Object getReceivedGroupId() {
+        return getReceivedFields().get(Field.GROUP_ID);
+    }
+
+    public Object getReceivedGroupSequence() {
+        return getReceivedFields().get(Field.GROUP_SEQUENCE);
+    }
+
+    public Object getReceivedReplyToGroupId() {
+        return getReceivedFields().get(Field.REPLY_TO_GROUP_ID);
+    }
+
+    @Override
+    protected Enum<?> getField(int fieldIndex) {
+        return Field.values()[fieldIndex];
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ReceivedMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ReceivedMatcher.java
new file mode 100644
index 0000000..9b70ae3
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ReceivedMatcher.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Received;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class ReceivedMatcher extends ListDescribedTypeMatcher {
+
+    public ReceivedMatcher() {
+        super(Received.Field.values().length, Received.DESCRIPTOR_CODE, Received.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Received.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public ReceivedMatcher withSectionNumber(int sectionNo) {
+        return withSectionNumber(equalTo(UnsignedInteger.valueOf(sectionNo)));
+    }
+
+    public ReceivedMatcher withSectionNumber(long sectionNo) {
+        return withSectionNumber(equalTo(UnsignedInteger.valueOf(sectionNo)));
+    }
+
+    public ReceivedMatcher withSectionNumber(UnsignedInteger sectionNo) {
+        return withSectionNumber(equalTo(sectionNo));
+    }
+
+    public ReceivedMatcher withSectionOffset(int sectionOffset) {
+        return withSectionOffset(equalTo(UnsignedLong.valueOf(sectionOffset)));
+    }
+
+    public ReceivedMatcher withSectionOffset(long sectionOffset) {
+        return withSectionOffset(equalTo(UnsignedLong.valueOf(sectionOffset)));
+    }
+
+    public ReceivedMatcher withSectionOffset(UnsignedLong sectionOffset) {
+        return withSectionOffset(equalTo(sectionOffset));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public ReceivedMatcher withSectionNumber(Matcher<?> m) {
+        addFieldMatcher(Received.Field.SECTION_NUMBER, m);
+        return this;
+    }
+
+    public ReceivedMatcher withSectionOffset(Matcher<?> m) {
+        addFieldMatcher(Received.Field.SECTION_OFFSET, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/RejectedMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/RejectedMatcher.java
new file mode 100644
index 0000000..7e32cbd
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/RejectedMatcher.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Rejected;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transport.ErrorConditionMatcher;
+import org.hamcrest.Matcher;
+
+public class RejectedMatcher extends ListDescribedTypeMatcher {
+
+    public RejectedMatcher() {
+        super(Rejected.Field.values().length, Rejected.DESCRIPTOR_CODE, Rejected.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Rejected.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public RejectedMatcher withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public RejectedMatcher withError(String condition) {
+        return withError(new ErrorConditionMatcher().withCondition(condition));
+    }
+
+    public RejectedMatcher withError(Symbol condition) {
+        return withError(new ErrorConditionMatcher().withCondition(condition));
+    }
+
+    public RejectedMatcher withError(String condition, String description) {
+        return withError(new ErrorConditionMatcher().withCondition(condition).withDescription(description));
+    }
+
+    public RejectedMatcher withError(String condition, String description, Map<String, Object> info) {
+        return withError(new ErrorConditionMatcher().withCondition(condition).withDescription(description).withInfo(info));
+    }
+
+    public RejectedMatcher withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(new ErrorConditionMatcher().withCondition(condition).withDescription(description).withInfoMap(info));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public RejectedMatcher withError(Matcher<?> m) {
+        addFieldMatcher(Rejected.Field.ERROR, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ReleasedMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ReleasedMatcher.java
new file mode 100644
index 0000000..56c2fae
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/ReleasedMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Released;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class ReleasedMatcher extends ListDescribedTypeMatcher {
+
+    public ReleasedMatcher() {
+        super(0, Released.DESCRIPTOR_CODE, Released.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Released.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/SourceMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/SourceMatcher.java
new file mode 100644
index 0000000..d4f952b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/SourceMatcher.java
@@ -0,0 +1,252 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class SourceMatcher extends ListDescribedTypeMatcher {
+
+    public SourceMatcher() {
+        super(Source.Field.values().length, Source.DESCRIPTOR_CODE, Source.DESCRIPTOR_SYMBOL);
+    }
+
+    public SourceMatcher(Source source) {
+        super(Source.Field.values().length, Source.DESCRIPTOR_CODE, Source.DESCRIPTOR_SYMBOL);
+
+        addSourceMatchers(source);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Source.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SourceMatcher withAddress(String name) {
+        return withAddress(equalTo(name));
+    }
+
+    public SourceMatcher withDurable(TerminusDurability durability) {
+        return withDurable(equalTo(durability.getValue()));
+    }
+
+    public SourceMatcher withExpiryPolicy(TerminusExpiryPolicy expiry) {
+        return withExpiryPolicy(equalTo(expiry.getPolicy()));
+    }
+
+    public SourceMatcher withTimeout(int timeout) {
+        return withTimeout(equalTo(UnsignedInteger.valueOf(timeout)));
+    }
+
+    public SourceMatcher withTimeout(long timeout) {
+        return withTimeout(equalTo(UnsignedInteger.valueOf(timeout)));
+    }
+
+    public SourceMatcher withTimeout(UnsignedInteger timeout) {
+        return withTimeout(equalTo(timeout));
+    }
+
+    public SourceMatcher withDefaultTimeout() {
+        return withTimeout(anyOf(nullValue(), equalTo(UnsignedInteger.ZERO)));
+    }
+
+    public SourceMatcher withDynamic(boolean dynamic) {
+        return withDynamic(equalTo(dynamic));
+    }
+
+    public SourceMatcher withDynamicNodePropertiesMap(Map<Symbol, Object> properties) {
+        return withDynamicNodeProperties(equalTo(properties));
+    }
+
+    public SourceMatcher withDynamicNodeProperties(Map<String, Object> properties) {
+        return withDynamicNodeProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    public SourceMatcher withDistributionMode(String distributionMode) {
+        return withDistributionMode(equalTo(Symbol.valueOf(distributionMode)));
+    }
+
+    public SourceMatcher withDistributionMode(Symbol distributionMode) {
+        return withDistributionMode(equalTo(distributionMode));
+    }
+
+    public SourceMatcher withFilter(Map<String, Object> filter) {
+        return withFilter(equalTo(TypeMapper.toSymbolKeyedMap(filter)));
+    }
+
+    public SourceMatcher withFilterMap(Map<Symbol, Object> filter) {
+        return withFilter(equalTo(filter));
+    }
+
+    public SourceMatcher withDefaultOutcome(DeliveryState defaultOutcome) {
+        return withDefaultOutcome(equalTo(defaultOutcome));
+    }
+
+    public SourceMatcher withOutcomes(String... outcomes) {
+        return withOutcomes(equalTo(TypeMapper.toSymbolArray(outcomes)));
+    }
+
+    public SourceMatcher withOutcomes(Symbol... outcomes) {
+        return withOutcomes(equalTo(outcomes));
+    }
+
+    public SourceMatcher withCapabilities(String... capabilities) {
+        return withCapabilities(equalTo(TypeMapper.toSymbolArray(capabilities)));
+    }
+
+    public SourceMatcher withCapabilities(Symbol... capabilities) {
+        return withCapabilities(equalTo(capabilities));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SourceMatcher withAddress(Matcher<?> m) {
+        addFieldMatcher(Source.Field.ADDRESS, m);
+        return this;
+    }
+
+    public SourceMatcher withDurable(Matcher<?> m) {
+        addFieldMatcher(Source.Field.DURABLE, m);
+        return this;
+    }
+
+    public SourceMatcher withExpiryPolicy(Matcher<?> m) {
+        addFieldMatcher(Source.Field.EXPIRY_POLICY, m);
+        return this;
+    }
+
+    public SourceMatcher withTimeout(Matcher<?> m) {
+        addFieldMatcher(Source.Field.TIMEOUT, m);
+        return this;
+    }
+
+    public SourceMatcher withDynamic(Matcher<?> m) {
+        addFieldMatcher(Source.Field.DYNAMIC, m);
+        return this;
+    }
+
+    public SourceMatcher withDynamicNodeProperties(Matcher<?> m) {
+        addFieldMatcher(Source.Field.DYNAMIC_NODE_PROPERTIES, m);
+        return this;
+    }
+
+    public SourceMatcher withDistributionMode(Matcher<?> m) {
+        addFieldMatcher(Source.Field.DISTRIBUTION_MODE, m);
+        return this;
+    }
+
+    public SourceMatcher withFilter(Matcher<?> m) {
+        addFieldMatcher(Source.Field.FILTER, m);
+        return this;
+    }
+
+    public SourceMatcher withDefaultOutcome(Matcher<?> m) {
+        addFieldMatcher(Source.Field.DEFAULT_OUTCOME, m);
+        return this;
+    }
+
+    public SourceMatcher withOutcomes(Matcher<?> m) {
+        addFieldMatcher(Source.Field.OUTCOMES, m);
+        return this;
+    }
+
+    public SourceMatcher withCapabilities(Matcher<?> m) {
+        addFieldMatcher(Source.Field.CAPABILITIES, m);
+        return this;
+    }
+
+    //----- Populate the matcher from a given Source object
+
+    private void addSourceMatchers(Source source) {
+        if (source.getAddress() != null) {
+            addFieldMatcher(Source.Field.ADDRESS, equalTo(source.getAddress()));
+        } else {
+            addFieldMatcher(Source.Field.ADDRESS, nullValue());
+        }
+
+        if (source.getDurable() != null) {
+            addFieldMatcher(Source.Field.DURABLE, equalTo(source.getDurable()));
+        } else {
+            addFieldMatcher(Source.Field.DURABLE, nullValue());
+        }
+
+        if (source.getExpiryPolicy() != null) {
+            addFieldMatcher(Source.Field.EXPIRY_POLICY, equalTo(source.getExpiryPolicy()));
+        } else {
+            addFieldMatcher(Source.Field.EXPIRY_POLICY, nullValue());
+        }
+
+        if (source.getTimeout() != null) {
+            addFieldMatcher(Source.Field.TIMEOUT, equalTo(source.getTimeout()));
+        } else {
+            addFieldMatcher(Source.Field.TIMEOUT, nullValue());
+        }
+
+        addFieldMatcher(Source.Field.DYNAMIC, equalTo(source.getDynamic()));
+
+        if (source.getDynamicNodeProperties() != null) {
+            addFieldMatcher(Source.Field.DYNAMIC_NODE_PROPERTIES, equalTo(source.getDynamicNodeProperties()));
+        } else {
+            addFieldMatcher(Source.Field.DYNAMIC_NODE_PROPERTIES, nullValue());
+        }
+
+        if (source.getDistributionMode() != null) {
+            addFieldMatcher(Source.Field.DISTRIBUTION_MODE, equalTo(source.getDistributionMode()));
+        } else {
+            addFieldMatcher(Source.Field.DISTRIBUTION_MODE, nullValue());
+        }
+
+        if (source.getFilter() != null) {
+            addFieldMatcher(Source.Field.FILTER, equalTo(source.getFilter()));
+        } else {
+            addFieldMatcher(Source.Field.FILTER, nullValue());
+        }
+
+        if (source.getDefaultOutcome() != null) {
+            addFieldMatcher(Source.Field.DEFAULT_OUTCOME, equalTo((DeliveryState) source.getDefaultOutcome()));
+        } else {
+            addFieldMatcher(Source.Field.DEFAULT_OUTCOME, nullValue());
+        }
+
+        if (source.getOutcomes() != null) {
+            addFieldMatcher(Source.Field.OUTCOMES, equalTo(source.getOutcomes()));
+        } else {
+            addFieldMatcher(Source.Field.OUTCOMES, nullValue());
+        }
+
+        if (source.getCapabilities() != null) {
+            addFieldMatcher(Source.Field.CAPABILITIES, equalTo(source.getCapabilities()));
+        } else {
+            addFieldMatcher(Source.Field.CAPABILITIES, nullValue());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/TargetMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/TargetMatcher.java
new file mode 100644
index 0000000..b81de3e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/messaging/TargetMatcher.java
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.messaging;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class TargetMatcher extends ListDescribedTypeMatcher {
+
+    public TargetMatcher() {
+        super(Target.Field.values().length, Target.DESCRIPTOR_CODE, Target.DESCRIPTOR_SYMBOL);
+    }
+
+    public TargetMatcher(Target target) {
+        super(Target.Field.values().length, Target.DESCRIPTOR_CODE, Target.DESCRIPTOR_SYMBOL);
+
+        addTargetMatchers(target);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Target.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public TargetMatcher withAddress(String name) {
+        return withAddress(equalTo(name));
+    }
+
+    public TargetMatcher withDurable(TerminusDurability durability) {
+        return withDurable(equalTo(durability.getValue()));
+    }
+
+    public TargetMatcher withExpiryPolicy(TerminusExpiryPolicy expiry) {
+        return withExpiryPolicy(equalTo(expiry.getPolicy()));
+    }
+
+    public TargetMatcher withTimeout(int timeout) {
+        return withTimeout(equalTo(UnsignedInteger.valueOf(timeout)));
+    }
+
+    public TargetMatcher withTimeout(long timeout) {
+        return withTimeout(equalTo(UnsignedInteger.valueOf(timeout)));
+    }
+
+    public TargetMatcher withTimeout(UnsignedInteger timeout) {
+        return withTimeout(equalTo(timeout));
+    }
+
+    public TargetMatcher withDefaultTimeout() {
+        return withTimeout(anyOf(nullValue(), equalTo(UnsignedInteger.ZERO)));
+    }
+
+    public TargetMatcher withDynamic(boolean dynamic) {
+        return withDynamic(equalTo(dynamic));
+    }
+
+    public TargetMatcher withDynamicNodeProperties(Map<String, Object> properties) {
+        return withDynamicNodeProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    public TargetMatcher withDynamicNodePropertiesMap(Map<Symbol, Object> properties) {
+        return withDynamicNodeProperties(equalTo(properties));
+    }
+
+    public TargetMatcher withCapabilities(String... capabilities) {
+        return withCapabilities(equalTo(TypeMapper.toSymbolArray(capabilities)));
+    }
+
+    public TargetMatcher withCapabilities(Symbol... capabilities) {
+        return withCapabilities(equalTo(capabilities));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public TargetMatcher withAddress(Matcher<?> m) {
+        addFieldMatcher(Target.Field.ADDRESS, m);
+        return this;
+    }
+
+    public TargetMatcher withDurable(Matcher<?> m) {
+        addFieldMatcher(Target.Field.DURABLE, m);
+        return this;
+    }
+
+    public TargetMatcher withExpiryPolicy(Matcher<?> m) {
+        addFieldMatcher(Target.Field.EXPIRY_POLICY, m);
+        return this;
+    }
+
+    public TargetMatcher withTimeout(Matcher<?> m) {
+        addFieldMatcher(Target.Field.TIMEOUT, m);
+        return this;
+    }
+
+    public TargetMatcher withDynamic(Matcher<?> m) {
+        addFieldMatcher(Target.Field.DYNAMIC, m);
+        return this;
+    }
+
+    public TargetMatcher withDynamicNodeProperties(Matcher<?> m) {
+        addFieldMatcher(Target.Field.DYNAMIC_NODE_PROPERTIES, m);
+        return this;
+    }
+
+    public TargetMatcher withCapabilities(Matcher<?> m) {
+        addFieldMatcher(Target.Field.CAPABILITIES, m);
+        return this;
+    }
+
+    //----- Populate the matcher from a given Source object
+
+    private void addTargetMatchers(Target target) {
+        if (target.getAddress() != null) {
+            addFieldMatcher(Target.Field.ADDRESS, equalTo(target.getAddress()));
+        } else {
+            addFieldMatcher(Target.Field.ADDRESS, nullValue());
+        }
+
+        if (target.getDurable() != null) {
+            addFieldMatcher(Target.Field.DURABLE, equalTo(target.getDurable()));
+        } else {
+            addFieldMatcher(Target.Field.DURABLE, nullValue());
+        }
+
+        if (target.getExpiryPolicy() != null) {
+            addFieldMatcher(Target.Field.EXPIRY_POLICY, equalTo(target.getExpiryPolicy()));
+        } else {
+            addFieldMatcher(Target.Field.EXPIRY_POLICY, nullValue());
+        }
+
+        if (target.getTimeout() != null) {
+            addFieldMatcher(Target.Field.TIMEOUT, equalTo(target.getTimeout()));
+        } else {
+            addFieldMatcher(Target.Field.TIMEOUT, nullValue());
+        }
+
+        addFieldMatcher(Target.Field.DYNAMIC, equalTo(target.getDynamic()));
+
+        if (target.getDynamicNodeProperties() != null) {
+            addFieldMatcher(Target.Field.DYNAMIC_NODE_PROPERTIES, equalTo(target.getDynamicNodeProperties()));
+        } else {
+            addFieldMatcher(Target.Field.DYNAMIC_NODE_PROPERTIES, nullValue());
+        }
+
+        if (target.getCapabilities() != null) {
+            addFieldMatcher(Target.Field.CAPABILITIES, equalTo(target.getCapabilities()));
+        } else {
+            addFieldMatcher(Target.Field.CAPABILITIES, nullValue());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslChallengeMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslChallengeMatcher.java
new file mode 100644
index 0000000..60cca7b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslChallengeMatcher.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.security;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslChallenge;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class SaslChallengeMatcher extends ListDescribedTypeMatcher {
+
+    public SaslChallengeMatcher() {
+        super(SaslChallenge.Field.values().length, SaslChallenge.DESCRIPTOR_CODE, SaslChallenge.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return SaslChallenge.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslChallengeMatcher withChallenge(byte[] challenge) {
+        return withChallenge(equalTo(new Binary(challenge)));
+    }
+
+    public SaslChallengeMatcher withChallenge(Binary challenge) {
+        return withChallenge(equalTo(challenge));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslChallengeMatcher withChallenge(Matcher<?> m) {
+        addFieldMatcher(SaslChallenge.Field.CHALLENGE, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslInitMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslInitMatcher.java
new file mode 100644
index 0000000..f794c3b
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslInitMatcher.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.security;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslInit;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class SaslInitMatcher extends ListDescribedTypeMatcher {
+
+    public SaslInitMatcher() {
+        super(SaslInit.Field.values().length, SaslInit.DESCRIPTOR_CODE, SaslInit.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return SaslInit.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslInitMatcher withMechanism(String mechanism) {
+        return withMechanism(equalTo(Symbol.valueOf(mechanism)));
+    }
+
+    public SaslInitMatcher withMechanism(Symbol mechanism) {
+        return withMechanism(equalTo(mechanism));
+    }
+
+    public SaslInitMatcher withInitialResponse(byte[] initialResponse) {
+        return withInitialResponse(equalTo(new Binary(initialResponse)));
+    }
+
+    public SaslInitMatcher withInitialResponse(Binary initialResponse) {
+        return withInitialResponse(equalTo(initialResponse));
+    }
+
+    public SaslInitMatcher withHostname(String hostname) {
+        return withHostname(equalTo(hostname));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslInitMatcher withMechanism(Matcher<?> m) {
+        addFieldMatcher(SaslInit.Field.MECHANISM, m);
+        return this;
+    }
+
+    public SaslInitMatcher withInitialResponse(Matcher<?> m) {
+        addFieldMatcher(SaslInit.Field.INITIAL_RESPONSE, m);
+        return this;
+    }
+
+    public SaslInitMatcher withHostname(Matcher<?> m) {
+        addFieldMatcher(SaslInit.Field.HOSTNAME, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslMechanismsMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslMechanismsMatcher.java
new file mode 100644
index 0000000..ec96fed
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslMechanismsMatcher.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.security;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslMechanisms;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class SaslMechanismsMatcher extends ListDescribedTypeMatcher {
+
+    public SaslMechanismsMatcher() {
+        super(SaslMechanisms.Field.values().length, SaslMechanisms.DESCRIPTOR_CODE, SaslMechanisms.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return SaslMechanisms.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslMechanismsMatcher withSaslServerMechanisms(String... mechanisms) {
+        return withSaslServerMechanisms(equalTo(TypeMapper.toSymbolArray(mechanisms)));
+    }
+
+    public SaslMechanismsMatcher withSaslServerMechanisms(Symbol... mechanisms) {
+        return withSaslServerMechanisms(equalTo(mechanisms));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslMechanismsMatcher withSaslServerMechanisms(Matcher<?> m) {
+        addFieldMatcher(SaslMechanisms.Field.SASL_SERVER_MECHANISMS, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslOutcomeMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslOutcomeMatcher.java
new file mode 100644
index 0000000..74e81d9
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslOutcomeMatcher.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.security;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslCode;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslOutcome;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class SaslOutcomeMatcher extends ListDescribedTypeMatcher {
+
+    public SaslOutcomeMatcher() {
+        super(SaslOutcome.Field.values().length, SaslOutcome.DESCRIPTOR_CODE, SaslOutcome.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return SaslOutcome.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslOutcomeMatcher withCode(byte code) {
+        return withCode(equalTo(SaslCode.valueOf(code)));
+    }
+
+    public SaslOutcomeMatcher withCode(SaslCode code) {
+        return withCode(equalTo(code));
+    }
+
+    public SaslOutcomeMatcher withAdditionalData(byte[] additionalData) {
+        return withAdditionalData(equalTo(new Binary(additionalData)));
+    }
+
+    public SaslOutcomeMatcher withAdditionalData(Binary additionalData) {
+        return withAdditionalData(equalTo(additionalData));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslOutcomeMatcher withCode(Matcher<?> m) {
+        addFieldMatcher(SaslOutcome.Field.CODE, m);
+        return this;
+    }
+
+    public SaslOutcomeMatcher withAdditionalData(Matcher<?> m) {
+        addFieldMatcher(SaslOutcome.Field.ADDITIONAL_DATA, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslResponseMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslResponseMatcher.java
new file mode 100644
index 0000000..051c54a
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/security/SaslResponseMatcher.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.security;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.security.SaslResponse;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class SaslResponseMatcher extends ListDescribedTypeMatcher {
+
+    public SaslResponseMatcher() {
+        super(SaslResponse.Field.values().length, SaslResponse.DESCRIPTOR_CODE, SaslResponse.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return SaslResponse.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public SaslResponseMatcher withResponse(byte[] response) {
+        return withResponse(equalTo(new Binary(response)));
+    }
+
+    public SaslResponseMatcher withResponse(Binary response) {
+        return withResponse(equalTo(response));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public SaslResponseMatcher withResponse(Matcher<?> m) {
+        addFieldMatcher(SaslResponse.Field.RESPONSE, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/CoordinatorMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/CoordinatorMatcher.java
new file mode 100644
index 0000000..ef1fe50
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/CoordinatorMatcher.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transactions;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class CoordinatorMatcher extends ListDescribedTypeMatcher {
+
+    public CoordinatorMatcher() {
+        super(Coordinator.Field.values().length, Coordinator.DESCRIPTOR_CODE, Coordinator.DESCRIPTOR_SYMBOL);
+    }
+
+    public CoordinatorMatcher(Coordinator coordinator) {
+        super(Coordinator.Field.values().length, Coordinator.DESCRIPTOR_CODE, Coordinator.DESCRIPTOR_SYMBOL);
+
+        addCoordinatorMatchers(coordinator);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Coordinator.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public CoordinatorMatcher withCapabilities(Symbol... capabilities) {
+        return withCapabilities(equalTo(capabilities));
+    }
+
+    public CoordinatorMatcher withCapabilities(String... capabilities) {
+        return withCapabilities(equalTo(TypeMapper.toSymbolArray(capabilities)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public CoordinatorMatcher withCapabilities(Matcher<?> m) {
+        addFieldMatcher(Coordinator.Field.CAPABILITIES, m);
+        return this;
+    }
+
+    //----- Populate the matcher from a given Source object
+
+    private void addCoordinatorMatchers(Coordinator coordinator) {
+        if (coordinator.getCapabilities() != null) {
+            addFieldMatcher(Coordinator.Field.CAPABILITIES, equalTo(coordinator.getCapabilities()));
+        } else {
+            addFieldMatcher(Coordinator.Field.CAPABILITIES, nullValue());
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DeclareMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DeclareMatcher.java
new file mode 100644
index 0000000..f1f7a50
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DeclareMatcher.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transactions;
+
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declared;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+public class DeclareMatcher extends ListDescribedTypeMatcher {
+
+    public DeclareMatcher() {
+        super(Declared.Field.values().length, Declared.DESCRIPTOR_CODE, Declared.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Declared.class;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DeclaredMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DeclaredMatcher.java
new file mode 100644
index 0000000..65b66d5
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DeclaredMatcher.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transactions;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Declared;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class DeclaredMatcher extends ListDescribedTypeMatcher {
+
+    public DeclaredMatcher() {
+        super(Declared.Field.values().length, Declared.DESCRIPTOR_CODE, Declared.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Declared.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public DeclaredMatcher withTxnId(byte[] txnId) {
+        return withTxnId(equalTo(new Binary(txnId)));
+    }
+
+    public DeclaredMatcher withTxnId(Binary txnId) {
+        return withTxnId(equalTo(txnId));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public DeclaredMatcher withTxnId(Matcher<?> m) {
+        addFieldMatcher(Declared.Field.TXN_ID, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DischargeMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DischargeMatcher.java
new file mode 100644
index 0000000..6a14e30
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/DischargeMatcher.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transactions;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Discharge;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class DischargeMatcher extends ListDescribedTypeMatcher {
+
+    public DischargeMatcher() {
+        super(Discharge.Field.values().length, Discharge.DESCRIPTOR_CODE, Discharge.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Discharge.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public DischargeMatcher withTxnId(byte[] txnId) {
+        return withTxnId(equalTo(new Binary(txnId)));
+    }
+
+    public DischargeMatcher withTxnId(Binary txnId) {
+        return withTxnId(equalTo(txnId));
+    }
+
+    public DischargeMatcher withFail(boolean fail) {
+        return withFail(equalTo(fail));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public DischargeMatcher withTxnId(Matcher<?> m) {
+        addFieldMatcher(Discharge.Field.TXN_ID, m);
+        return this;
+    }
+
+    public DischargeMatcher withFail(Matcher<?> m) {
+        addFieldMatcher(Discharge.Field.FAIL, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/TransactionalStateMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/TransactionalStateMatcher.java
new file mode 100644
index 0000000..6dbcb8d
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transactions/TransactionalStateMatcher.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transactions;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.TransactionalState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class TransactionalStateMatcher extends ListDescribedTypeMatcher {
+
+    public TransactionalStateMatcher() {
+        super(TransactionalState.Field.values().length, TransactionalState.DESCRIPTOR_CODE, TransactionalState.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return TransactionalState.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public TransactionalStateMatcher withTxnId(byte[] txnId) {
+        return withTxnId(equalTo(new Binary(txnId)));
+    }
+
+    public TransactionalStateMatcher withTxnId(Binary txnId) {
+        return withTxnId(equalTo(txnId));
+    }
+
+    public TransactionalStateMatcher withOutcome(DeliveryState outcome) {
+        return withOutcome(equalTo(outcome));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public TransactionalStateMatcher withTxnId(Matcher<?> m) {
+        addFieldMatcher(TransactionalState.Field.TXN_ID, m);
+        return this;
+    }
+
+    public TransactionalStateMatcher withOutcome(Matcher<?> m) {
+        addFieldMatcher(TransactionalState.Field.OUTCOME, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/AttachMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/AttachMatcher.java
new file mode 100644
index 0000000..31b04d8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/AttachMatcher.java
@@ -0,0 +1,264 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.SenderSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.SourceMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.TargetMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transactions.CoordinatorMatcher;
+import org.hamcrest.Matcher;
+
+public class AttachMatcher extends ListDescribedTypeMatcher {
+
+    public AttachMatcher() {
+        super(Attach.Field.values().length, Attach.DESCRIPTOR_CODE, Attach.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Attach.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public AttachMatcher withName(String name) {
+        return withName(equalTo(name));
+    }
+
+    public AttachMatcher withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public AttachMatcher withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public AttachMatcher withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public AttachMatcher withRole(boolean role) {
+        return withRole(equalTo(role));
+    }
+
+    public AttachMatcher withRole(Boolean role) {
+        return withRole(equalTo(role));
+    }
+
+    public AttachMatcher withRole(Role role) {
+        return withRole(equalTo(role.getValue()));
+    }
+
+    public AttachMatcher withSndSettleMode(byte sndSettleMode) {
+        return withSndSettleMode(equalTo(SenderSettleMode.valueOf(sndSettleMode)));
+    }
+
+    public AttachMatcher withSndSettleMode(Byte sndSettleMode) {
+        return withSndSettleMode(sndSettleMode == null ? nullValue() : equalTo(UnsignedByte.valueOf(sndSettleMode.byteValue())));
+    }
+
+    public AttachMatcher withSndSettleMode(SenderSettleMode sndSettleMode) {
+        return withSndSettleMode(sndSettleMode == null ? nullValue() : equalTo(sndSettleMode.getValue()));
+    }
+
+    public AttachMatcher withRcvSettleMode(byte rcvSettleMode) {
+        return withRcvSettleMode(equalTo(ReceiverSettleMode.valueOf(rcvSettleMode)));
+    }
+
+    public AttachMatcher withRcvSettleMode(Byte rcvSettleMode) {
+        return withRcvSettleMode(rcvSettleMode == null ? nullValue() : equalTo(UnsignedByte.valueOf(rcvSettleMode.byteValue())));
+    }
+
+    public AttachMatcher withRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        return withRcvSettleMode(rcvSettleMode == null ? nullValue() : equalTo(rcvSettleMode.getValue()));
+    }
+
+    public AttachMatcher withSource(Source source) {
+        if (source != null) {
+            SourceMatcher sourceMatcher = new SourceMatcher(source);
+            return withSource(sourceMatcher);
+        } else {
+            return withSource(nullValue());
+        }
+    }
+
+    public AttachMatcher withTarget(Target target) {
+        if (target != null) {
+            TargetMatcher targetMatcher = new TargetMatcher(target);
+            return withTarget(targetMatcher);
+        } else {
+            return withTarget(nullValue());
+        }
+    }
+
+    public AttachMatcher withCoordinator(Coordinator coordinator) {
+        if (coordinator != null) {
+            CoordinatorMatcher coordinatorMatcher = new CoordinatorMatcher();
+            return withCoordinator(coordinatorMatcher);
+        } else {
+            return withCoordinator(nullValue());
+        }
+    }
+
+    public AttachMatcher withUnsettled(Map<Binary, DeliveryState> unsettled) {
+        return withUnsettled(equalTo(unsettled));
+    }
+
+    public AttachMatcher withIncompleteUnsettled(boolean incomplete) {
+        return withIncompleteUnsettled(equalTo(incomplete));
+    }
+
+    public AttachMatcher withInitialDeliveryCount(int initialDeliveryCount) {
+        return withInitialDeliveryCount(equalTo(UnsignedInteger.valueOf(initialDeliveryCount)));
+    }
+
+    public AttachMatcher withInitialDeliveryCount(long initialDeliveryCount) {
+        return withInitialDeliveryCount(equalTo(UnsignedInteger.valueOf(initialDeliveryCount)));
+    }
+
+    public AttachMatcher withInitialDeliveryCount(UnsignedInteger initialDeliveryCount) {
+        return withInitialDeliveryCount(equalTo(initialDeliveryCount));
+    }
+
+    public AttachMatcher withMaxMessageSize(long maxMessageSize) {
+        return withMaxMessageSize(equalTo(UnsignedLong.valueOf(maxMessageSize)));
+    }
+
+    public AttachMatcher withMaxMessageSize(UnsignedLong maxMessageSize) {
+        return withMaxMessageSize(equalTo(maxMessageSize));
+    }
+
+    public AttachMatcher withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public AttachMatcher withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public AttachMatcher withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public AttachMatcher withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public AttachMatcher withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public AttachMatcher withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public AttachMatcher withName(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.NAME, m);
+        return this;
+    }
+
+    public AttachMatcher withHandle(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.HANDLE, m);
+        return this;
+    }
+
+    public AttachMatcher withRole(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.ROLE, m);
+        return this;
+    }
+
+    public AttachMatcher withSndSettleMode(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.SND_SETTLE_MODE, m);
+        return this;
+    }
+
+    public AttachMatcher withRcvSettleMode(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.RCV_SETTLE_MODE, m);
+        return this;
+    }
+
+    public AttachMatcher withSource(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.SOURCE, m);
+        return this;
+    }
+
+    public AttachMatcher withTarget(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.TARGET, m);
+        return this;
+    }
+
+    public AttachMatcher withCoordinator(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.TARGET, m);
+        return this;
+    }
+
+    public AttachMatcher withUnsettled(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.UNSETTLED, m);
+        return this;
+    }
+
+    public AttachMatcher withIncompleteUnsettled(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.INCOMPLETE_UNSETTLED, m);
+        return this;
+    }
+
+    public AttachMatcher withInitialDeliveryCount(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.INITIAL_DELIVERY_COUNT, m);
+        return this;
+    }
+
+    public AttachMatcher withMaxMessageSize(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.MAX_MESSAGE_SIZE, m);
+        return this;
+    }
+
+    public AttachMatcher withOfferedCapabilities(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.OFFERED_CAPABILITIES, m);
+        return this;
+    }
+
+    public AttachMatcher withDesiredCapabilities(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.DESIRED_CAPABILITIES, m);
+        return this;
+    }
+
+    public AttachMatcher withProperties(Matcher<?> m) {
+        addFieldMatcher(Attach.Field.PROPERTIES, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/BeginMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/BeginMatcher.java
new file mode 100644
index 0000000..521dee5
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/BeginMatcher.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class BeginMatcher extends ListDescribedTypeMatcher {
+
+    public BeginMatcher() {
+        super(Begin.Field.values().length, Begin.DESCRIPTOR_CODE, Begin.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Begin.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public BeginMatcher withRemoteChannel(int remoteChannel) {
+        return withRemoteChannel(equalTo(UnsignedShort.valueOf((short) remoteChannel)));
+    }
+
+    public BeginMatcher withRemoteChannel(UnsignedShort remoteChannel) {
+        return withRemoteChannel(equalTo(remoteChannel));
+    }
+
+    public BeginMatcher withNextOutgoingId(int nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public BeginMatcher withNextOutgoingId(long nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public BeginMatcher withNextOutgoingId(UnsignedInteger nextOutgoingId) {
+        return withNextOutgoingId(equalTo(nextOutgoingId));
+    }
+
+    public BeginMatcher withIncomingWindow(int incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public BeginMatcher withIncomingWindow(long incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public BeginMatcher withIncomingWindow(UnsignedInteger incomingWindow) {
+        return withIncomingWindow(equalTo(incomingWindow));
+    }
+
+    public BeginMatcher withOutgoingWindow(int outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public BeginMatcher withOutgoingWindow(long outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public BeginMatcher withOutgoingWindow(UnsignedInteger outgoingWindow) {
+        return withOutgoingWindow(equalTo(outgoingWindow));
+    }
+
+    public BeginMatcher withHandleMax(int handleMax) {
+        return withHandleMax(equalTo(UnsignedInteger.valueOf(handleMax)));
+    }
+
+    public BeginMatcher withHandleMax(long handleMax) {
+        return withHandleMax(equalTo(UnsignedInteger.valueOf(handleMax)));
+    }
+
+    public BeginMatcher withHandleMax(UnsignedInteger handleMax) {
+        return withHandleMax(equalTo(handleMax));
+    }
+
+    public BeginMatcher withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public BeginMatcher withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public BeginMatcher withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public BeginMatcher withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public BeginMatcher withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public BeginMatcher withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public BeginMatcher withRemoteChannel(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.REMOTE_CHANNEL, m);
+        return this;
+    }
+
+    public BeginMatcher withNextOutgoingId(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.NEXT_OUTGOING_ID, m);
+        return this;
+    }
+
+    public BeginMatcher withIncomingWindow(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.INCOMING_WINDOW, m);
+        return this;
+    }
+
+    public BeginMatcher withOutgoingWindow(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.OUTGOING_WINDOW, m);
+        return this;
+    }
+
+    public BeginMatcher withHandleMax(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.HANDLE_MAX, m);
+        return this;
+    }
+
+    public BeginMatcher withOfferedCapabilities(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.OFFERED_CAPABILITIES, m);
+        return this;
+    }
+
+    public BeginMatcher withDesiredCapabilities(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.DESIRED_CAPABILITIES, m);
+        return this;
+    }
+
+    public BeginMatcher withProperties(Matcher<?> m) {
+        addFieldMatcher(Begin.Field.PROPERTIES, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/CloseMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/CloseMatcher.java
new file mode 100644
index 0000000..7319655
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/CloseMatcher.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Close;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class CloseMatcher extends ListDescribedTypeMatcher {
+
+    public CloseMatcher() {
+        super(Close.Field.values().length, Close.DESCRIPTOR_CODE, Close.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Close.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public CloseMatcher withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public CloseMatcher withError(String condition, String description) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description)));
+    }
+
+    public CloseMatcher withError(String condition, String description, Map<String, Object> info) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info))));
+    }
+
+    public CloseMatcher withError(Symbol condition, String description) {
+        return withError(equalTo(new ErrorCondition(condition, description)));
+    }
+
+    public CloseMatcher withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(equalTo(new ErrorCondition(condition, description, info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public CloseMatcher withError(Matcher<?> m) {
+        addFieldMatcher(End.Field.ERROR, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/DetachMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/DetachMatcher.java
new file mode 100644
index 0000000..9185c1e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/DetachMatcher.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Detach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class DetachMatcher extends ListDescribedTypeMatcher {
+
+    public DetachMatcher() {
+        super(Detach.Field.values().length, Detach.DESCRIPTOR_CODE, Detach.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Detach.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public DetachMatcher withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public DetachMatcher withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public DetachMatcher withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public DetachMatcher withClosed(boolean closed) {
+        return withClosed(equalTo(closed));
+    }
+
+    public DetachMatcher withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public DetachMatcher withError(String condition) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition))));
+    }
+
+    public DetachMatcher withError(String condition, String description) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description)));
+    }
+
+    public DetachMatcher withError(String condition, String description, Map<String, Object> info) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info))));
+    }
+
+    public DetachMatcher withError(Symbol condition, String description) {
+        return withError(equalTo(new ErrorCondition(condition, description)));
+    }
+
+    public DetachMatcher withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(equalTo(new ErrorCondition(condition, description, info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public DetachMatcher withHandle(Matcher<?> m) {
+        addFieldMatcher(Detach.Field.HANDLE, m);
+        return this;
+    }
+
+    public DetachMatcher withClosed(Matcher<?> m) {
+        addFieldMatcher(Detach.Field.CLOSED, m);
+        return this;
+    }
+
+    public DetachMatcher withError(Matcher<?> m) {
+        addFieldMatcher(Detach.Field.ERROR, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/DispositionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/DispositionMatcher.java
new file mode 100644
index 0000000..9e1f8c0
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/DispositionMatcher.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class DispositionMatcher extends ListDescribedTypeMatcher {
+
+    public DispositionMatcher() {
+        super(Disposition.Field.values().length, Disposition.DESCRIPTOR_CODE, Disposition.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Disposition.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public DispositionMatcher withRole(boolean role) {
+        return withRole(equalTo(role));
+    }
+
+    public DispositionMatcher withRole(Boolean role) {
+        return withRole(equalTo(role));
+    }
+
+    public DispositionMatcher withRole(Role role) {
+        return withRole(equalTo(role.getValue()));
+    }
+
+    public DispositionMatcher withFirst(int first) {
+        return withFirst(equalTo(UnsignedInteger.valueOf(first)));
+    }
+
+    public DispositionMatcher withFirst(long first) {
+        return withFirst(equalTo(UnsignedInteger.valueOf(first)));
+    }
+
+    public DispositionMatcher withFirst(UnsignedInteger first) {
+        return withFirst(equalTo(first));
+    }
+
+    public DispositionMatcher withLast(int last) {
+        return withLast(equalTo(UnsignedInteger.valueOf(last)));
+    }
+
+    public DispositionMatcher withLast(long last) {
+        return withLast(equalTo(UnsignedInteger.valueOf(last)));
+    }
+
+    public DispositionMatcher withLast(UnsignedInteger last) {
+        return withLast(equalTo(last));
+    }
+
+    public DispositionMatcher withSettled(boolean settled) {
+        return withSettled(equalTo(settled));
+    }
+
+    public DispositionMatcher withState(DeliveryState state) {
+        return withState(equalTo(state));
+    }
+
+    public DispositionMatcher withBatchable(boolean batchable) {
+        return withBatchable(equalTo(batchable));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public DispositionMatcher withRole(Matcher<?> m) {
+        addFieldMatcher(Disposition.Field.ROLE, m);
+        return this;
+    }
+
+    public DispositionMatcher withFirst(Matcher<?> m) {
+        addFieldMatcher(Disposition.Field.FIRST, m);
+        return this;
+    }
+
+    public DispositionMatcher withLast(Matcher<?> m) {
+        addFieldMatcher(Disposition.Field.LAST, m);
+        return this;
+    }
+
+    public DispositionMatcher withSettled(Matcher<?> m) {
+        addFieldMatcher(Disposition.Field.SETTLED, m);
+        return this;
+    }
+
+    public DispositionMatcher withState(Matcher<?> m) {
+        addFieldMatcher(Disposition.Field.STATE, m);
+        return this;
+    }
+
+    public DispositionMatcher withBatchable(Matcher<?> m) {
+        addFieldMatcher(Disposition.Field.BATCHABLE, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/EndMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/EndMatcher.java
new file mode 100644
index 0000000..434bdaa
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/EndMatcher.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transport.End;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class EndMatcher extends ListDescribedTypeMatcher {
+
+    public EndMatcher() {
+        super(End.Field.values().length, End.DESCRIPTOR_CODE, End.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return End.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public EndMatcher withError(ErrorCondition error) {
+        return withError(equalTo(error));
+    }
+
+    public EndMatcher withError(String condition, String description) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description)));
+    }
+
+    public EndMatcher withError(String condition, String description, Map<String, Object> info) {
+        return withError(equalTo(new ErrorCondition(Symbol.valueOf(condition), description, TypeMapper.toSymbolKeyedMap(info))));
+    }
+
+    public EndMatcher withError(Symbol condition, String description) {
+        return withError(equalTo(new ErrorCondition(condition, description)));
+    }
+
+    public EndMatcher withError(Symbol condition, String description, Map<Symbol, Object> info) {
+        return withError(equalTo(new ErrorCondition(condition, description, info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public EndMatcher withError(Matcher<?> m) {
+        addFieldMatcher(End.Field.ERROR, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/ErrorConditionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/ErrorConditionMatcher.java
new file mode 100644
index 0000000..cea4a17
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/ErrorConditionMatcher.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ErrorCondition;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class ErrorConditionMatcher extends ListDescribedTypeMatcher {
+
+    public ErrorConditionMatcher() {
+        super(ErrorCondition.Field.values().length, ErrorCondition.DESCRIPTOR_CODE, ErrorCondition.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return ErrorCondition.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public ErrorConditionMatcher withCondition(String condition) {
+        return withCondition(equalTo(Symbol.valueOf(condition)));
+    }
+
+    public ErrorConditionMatcher withCondition(Symbol condition) {
+        return withCondition(equalTo(condition));
+    }
+
+    public ErrorConditionMatcher withDescription(String description) {
+        return withDescription(equalTo(description));
+    }
+
+    public ErrorConditionMatcher withInfoMap(Map<Symbol, Object> info) {
+        return withInfo(equalTo(info));
+    }
+
+    public ErrorConditionMatcher withInfo(Map<String, Object> info) {
+        return withInfo(equalTo(TypeMapper.toSymbolKeyedMap(info)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public ErrorConditionMatcher withCondition(Matcher<?> m) {
+        addFieldMatcher(ErrorCondition.Field.CONDITION, m);
+        return this;
+    }
+
+    public ErrorConditionMatcher withDescription(Matcher<?> m) {
+        addFieldMatcher(ErrorCondition.Field.DESCRIPTION, m);
+        return this;
+    }
+
+    public ErrorConditionMatcher withInfo(Matcher<?> m) {
+        addFieldMatcher(ErrorCondition.Field.INFO, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/FlowMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/FlowMatcher.java
new file mode 100644
index 0000000..a3b1ada
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/FlowMatcher.java
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class FlowMatcher extends ListDescribedTypeMatcher {
+
+    public FlowMatcher() {
+        super(Flow.Field.values().length, Flow.DESCRIPTOR_CODE, Flow.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Flow.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public FlowMatcher withNextIncomingId(int nextIncomingId) {
+        return withNextIncomingId(equalTo(UnsignedInteger.valueOf(nextIncomingId)));
+    }
+
+    public FlowMatcher withNextIncomingId(long nextIncomingId) {
+        return withNextIncomingId(equalTo(UnsignedInteger.valueOf(nextIncomingId)));
+    }
+
+    public FlowMatcher withNextIncomingId(UnsignedInteger nextIncomingId) {
+        return withNextIncomingId(equalTo(nextIncomingId));
+    }
+
+    public FlowMatcher withIncomingWindow(int incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public FlowMatcher withIncomingWindow(long incomingWindow) {
+        return withIncomingWindow(equalTo(UnsignedInteger.valueOf(incomingWindow)));
+    }
+
+    public FlowMatcher withIncomingWindow(UnsignedInteger incomingWindow) {
+        return withIncomingWindow(equalTo(incomingWindow));
+    }
+
+    public FlowMatcher withNextOutgoingId(int nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public FlowMatcher withNextOutgoingId(long nextOutgoingId) {
+        return withNextOutgoingId(equalTo(UnsignedInteger.valueOf(nextOutgoingId)));
+    }
+
+    public FlowMatcher withNextOutgoingId(UnsignedInteger nextOutgoingId) {
+        return withNextOutgoingId(equalTo(nextOutgoingId));
+    }
+
+    public FlowMatcher withOutgoingWindow(int outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public FlowMatcher withOutgoingWindow(long outgoingWindow) {
+        return withOutgoingWindow(equalTo(UnsignedInteger.valueOf(outgoingWindow)));
+    }
+
+    public FlowMatcher withOutgoingWindow(UnsignedInteger outgoingWindow) {
+        return withOutgoingWindow(equalTo(outgoingWindow));
+    }
+
+    public FlowMatcher withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public FlowMatcher withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public FlowMatcher withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public FlowMatcher withDeliveryCount(int deliveryCount) {
+        return withDeliveryCount(equalTo(UnsignedInteger.valueOf(deliveryCount)));
+    }
+
+    public FlowMatcher withDeliveryCount(long deliveryCount) {
+        return withDeliveryCount(equalTo(UnsignedInteger.valueOf(deliveryCount)));
+    }
+
+    public FlowMatcher withDeliveryCount(UnsignedInteger deliveryCount) {
+        return withDeliveryCount(equalTo(deliveryCount));
+    }
+
+    public FlowMatcher withLinkCredit(int linkCredit) {
+        return withLinkCredit(equalTo(UnsignedInteger.valueOf(linkCredit)));
+    }
+
+    public FlowMatcher withLinkCredit(long linkCredit) {
+        return withLinkCredit(equalTo(UnsignedInteger.valueOf(linkCredit)));
+    }
+
+    public FlowMatcher withLinkCredit(UnsignedInteger linkCredit) {
+        return withLinkCredit(equalTo(linkCredit));
+    }
+
+    public FlowMatcher withAvailable(int available) {
+        return withAvailable(equalTo(UnsignedInteger.valueOf(available)));
+    }
+
+    public FlowMatcher withAvailable(long available) {
+        return withAvailable(equalTo(UnsignedInteger.valueOf(available)));
+    }
+
+    public FlowMatcher withAvailable(UnsignedInteger available) {
+        return withAvailable(equalTo(available));
+    }
+
+    public FlowMatcher withDrain(boolean drain) {
+        return withDrain(equalTo(drain));
+    }
+
+    public FlowMatcher withEcho(boolean echo) {
+        return withEcho(equalTo(echo));
+    }
+
+    public FlowMatcher withProperties(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public FlowMatcher withNextIncomingId(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.NEXT_INCOMING_ID, m);
+        return this;
+    }
+
+    public FlowMatcher withIncomingWindow(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.INCOMING_WINDOW, m);
+        return this;
+    }
+
+    public FlowMatcher withNextOutgoingId(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.NEXT_OUTGOING_ID, m);
+        return this;
+    }
+
+    public FlowMatcher withOutgoingWindow(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.OUTGOING_WINDOW, m);
+        return this;
+    }
+
+    public FlowMatcher withHandle(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.HANDLE, m);
+        return this;
+    }
+
+    public FlowMatcher withDeliveryCount(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.DELIVERY_COUNT, m);
+        return this;
+    }
+
+    public FlowMatcher withLinkCredit(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.LINK_CREDIT, m);
+        return this;
+    }
+
+    public FlowMatcher withAvailable(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.AVAILABLE, m);
+        return this;
+    }
+
+    public FlowMatcher withDrain(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.DRAIN, m);
+        return this;
+    }
+
+    public FlowMatcher withEcho(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.ECHO, m);
+        return this;
+    }
+
+    public FlowMatcher withProperties(Matcher<?> m) {
+        addFieldMatcher(Flow.Field.PROPERTIES, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/HeartBeatMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/HeartBeatMatcher.java
new file mode 100644
index 0000000..09446da
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/HeartBeatMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import org.apache.qpid.protonj2.test.driver.codec.ListDescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.transport.HeartBeat;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+
+/**
+ * Matcher used for validation of Heart Beat frames
+ */
+public class HeartBeatMatcher extends ListDescribedTypeMatcher {
+
+    public HeartBeatMatcher() {
+        super(0, null, null);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return HeartBeat.class;
+    }
+
+    @Override
+    protected boolean matchesSafely(ListDescribedType received) {
+        return received instanceof HeartBeat;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/OpenMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/OpenMatcher.java
new file mode 100644
index 0000000..50f2e07
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/OpenMatcher.java
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.util.TypeMapper;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class OpenMatcher extends ListDescribedTypeMatcher {
+
+    public OpenMatcher() {
+        super(Open.Field.values().length, Open.DESCRIPTOR_CODE, Open.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Open.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public OpenMatcher withContainerId(String container) {
+        return withContainerId(equalTo(container));
+    }
+
+    public OpenMatcher withHostname(String hostname) {
+        return withHostname(equalTo(hostname));
+    }
+
+    public OpenMatcher withMaxFrameSize(int maxFrameSize) {
+        return withMaxFrameSize(equalTo(UnsignedInteger.valueOf(maxFrameSize)));
+    }
+
+    public OpenMatcher withMaxFrameSize(long maxFrameSize) {
+        return withMaxFrameSize(equalTo(UnsignedInteger.valueOf(maxFrameSize)));
+    }
+
+    public OpenMatcher withMaxFrameSize(UnsignedInteger maxFrameSize) {
+        return withMaxFrameSize(equalTo(maxFrameSize));
+    }
+
+    public OpenMatcher withChannelMax(short channelMax) {
+        return withChannelMax(equalTo(UnsignedShort.valueOf(channelMax)));
+    }
+
+    public OpenMatcher withChannelMax(int channelMax) {
+        return withChannelMax(equalTo(UnsignedShort.valueOf(channelMax)));
+    }
+
+    public OpenMatcher withChannelMax(UnsignedShort channelMax) {
+        return withChannelMax(equalTo(channelMax));
+    }
+
+    public OpenMatcher withIdleTimeOut(int idleTimeout) {
+        return withIdleTimeOut(equalTo(UnsignedInteger.valueOf(idleTimeout)));
+    }
+
+    public OpenMatcher withIdleTimeOut(long idleTimeout) {
+        return withIdleTimeOut(equalTo(UnsignedInteger.valueOf(idleTimeout)));
+    }
+
+    public OpenMatcher withIdleTimeOut(UnsignedInteger idleTimeout) {
+        return withIdleTimeOut(equalTo(idleTimeout));
+    }
+
+    public OpenMatcher withOutgoingLocales(String... outgoingLocales) {
+        return withOutgoingLocales(equalTo(TypeMapper.toSymbolArray(outgoingLocales)));
+    }
+
+    public OpenMatcher withOutgoingLocales(Symbol... outgoingLocales) {
+        return withOutgoingLocales(equalTo(outgoingLocales));
+    }
+
+    public OpenMatcher withIncomingLocales(String... incomingLocales) {
+        return withIncomingLocales(equalTo(TypeMapper.toSymbolArray(incomingLocales)));
+    }
+
+    public OpenMatcher withIncomingLocales(Symbol... incomingLocales) {
+        return withIncomingLocales(equalTo(incomingLocales));
+    }
+
+    public OpenMatcher withOfferedCapabilities(String... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(TypeMapper.toSymbolArray(offeredCapabilities)));
+    }
+
+    public OpenMatcher withOfferedCapabilities(Symbol... offeredCapabilities) {
+        return withOfferedCapabilities(equalTo(offeredCapabilities));
+    }
+
+    public OpenMatcher withDesiredCapabilities(String... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(TypeMapper.toSymbolArray(desiredCapabilities)));
+    }
+
+    public OpenMatcher withDesiredCapabilities(Symbol... desiredCapabilities) {
+        return withDesiredCapabilities(equalTo(desiredCapabilities));
+    }
+
+    public OpenMatcher withPropertiesMap(Map<Symbol, Object> properties) {
+        return withProperties(equalTo(properties));
+    }
+
+    public OpenMatcher withProperties(Map<String, Object> properties) {
+        return withProperties(equalTo(TypeMapper.toSymbolKeyedMap(properties)));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public OpenMatcher withContainerId(Matcher<?> m) {
+        addFieldMatcher(Open.Field.CONTAINER_ID, m);
+        return this;
+    }
+
+    public OpenMatcher withHostname(Matcher<?> m) {
+        addFieldMatcher(Open.Field.HOSTNAME, m);
+        return this;
+    }
+
+    public OpenMatcher withMaxFrameSize(Matcher<?> m) {
+        addFieldMatcher(Open.Field.MAX_FRAME_SIZE, m);
+        return this;
+    }
+
+    public OpenMatcher withChannelMax(Matcher<?> m) {
+        addFieldMatcher(Open.Field.CHANNEL_MAX, m);
+        return this;
+    }
+
+    public OpenMatcher withIdleTimeOut(Matcher<?> m) {
+        addFieldMatcher(Open.Field.IDLE_TIME_OUT, m);
+        return this;
+    }
+
+    public OpenMatcher withOutgoingLocales(Matcher<?> m) {
+        addFieldMatcher(Open.Field.OUTGOING_LOCALES, m);
+        return this;
+    }
+
+    public OpenMatcher withIncomingLocales(Matcher<?> m) {
+        addFieldMatcher(Open.Field.INCOMING_LOCALES, m);
+        return this;
+    }
+
+    public OpenMatcher withOfferedCapabilities(Matcher<?> m) {
+        addFieldMatcher(Open.Field.OFFERED_CAPABILITIES, m);
+        return this;
+    }
+
+    public OpenMatcher withDesiredCapabilities(Matcher<?> m) {
+        addFieldMatcher(Open.Field.DESIRED_CAPABILITIES, m);
+        return this;
+    }
+
+    public OpenMatcher withProperties(Matcher<?> m) {
+        addFieldMatcher(Open.Field.PROPERTIES, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/TransferMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/TransferMatcher.java
new file mode 100644
index 0000000..3a3de77
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/TransferMatcher.java
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.transport.DeliveryState;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+import org.apache.qpid.protonj2.test.driver.matchers.ListDescribedTypeMatcher;
+import org.hamcrest.Matcher;
+
+public class TransferMatcher extends ListDescribedTypeMatcher {
+
+    public TransferMatcher() {
+        super(Transfer.Field.values().length, Transfer.DESCRIPTOR_CODE, Transfer.DESCRIPTOR_SYMBOL);
+    }
+
+    @Override
+    protected Class<?> getDescribedTypeClass() {
+        return Transfer.class;
+    }
+
+    //----- Type specific with methods that perform simple equals checks
+
+    public TransferMatcher withHandle(int handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public TransferMatcher withHandle(long handle) {
+        return withHandle(equalTo(UnsignedInteger.valueOf(handle)));
+    }
+
+    public TransferMatcher withHandle(UnsignedInteger handle) {
+        return withHandle(equalTo(handle));
+    }
+
+    public TransferMatcher withDeliveryId(int deliveryId) {
+        return withDeliveryId(equalTo(UnsignedInteger.valueOf(deliveryId)));
+    }
+
+    public TransferMatcher withDeliveryId(long deliveryId) {
+        return withDeliveryId(equalTo(UnsignedInteger.valueOf(deliveryId)));
+    }
+
+    public TransferMatcher withDeliveryId(UnsignedInteger deliveryId) {
+        return withDeliveryId(equalTo(deliveryId));
+    }
+
+    public TransferMatcher withDeliveryTag(byte[] tag) {
+        return withDeliveryTag(new Binary(tag));
+    }
+
+    public TransferMatcher withDeliveryTag(Binary deliveryTag) {
+        return withDeliveryTag(equalTo(deliveryTag));
+    }
+
+    public TransferMatcher withMessageFormat(int messageFormat) {
+        return withMessageFormat(equalTo(UnsignedInteger.valueOf(messageFormat)));
+    }
+
+    public TransferMatcher withMessageFormat(long messageFormat) {
+        return withMessageFormat(equalTo(UnsignedInteger.valueOf(messageFormat)));
+    }
+
+    public TransferMatcher withMessageFormat(UnsignedInteger messageFormat) {
+        return withMessageFormat(equalTo(messageFormat));
+    }
+
+    public TransferMatcher withSettled(boolean settled) {
+        return withSettled(equalTo(settled));
+    }
+
+    public TransferMatcher withMore(boolean more) {
+        return withMore(equalTo(more));
+    }
+
+    public TransferMatcher withRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        return withRcvSettleMode(equalTo(rcvSettleMode.getValue()));
+    }
+
+    public TransferMatcher withState(DeliveryState state) {
+        return withState(equalTo(state));
+    }
+
+    public TransferMatcher withNullState() {
+        return withState(nullValue());
+    }
+
+    public TransferMatcher withResume(boolean resume) {
+        return withResume(equalTo(resume));
+    }
+
+    public TransferMatcher withAborted(boolean aborted) {
+        return withAborted(equalTo(aborted));
+    }
+
+    public TransferMatcher withBatchable(boolean batchable) {
+        return withBatchable(equalTo(batchable));
+    }
+
+    //----- Matcher based with methods for more complex validation
+
+    public TransferMatcher withHandle(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.HANDLE, m);
+        return this;
+    }
+
+    public TransferMatcher withDeliveryId(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.DELIVERY_ID, m);
+        return this;
+    }
+
+    public TransferMatcher withDeliveryTag(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.DELIVERY_TAG, m);
+        return this;
+    }
+
+    public TransferMatcher withMessageFormat(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.MESSAGE_FORMAT, m);
+        return this;
+    }
+
+    public TransferMatcher withSettled(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.SETTLED, m);
+        return this;
+    }
+
+    public TransferMatcher withMore(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.MORE, m);
+        return this;
+    }
+
+    public TransferMatcher withRcvSettleMode(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.RCV_SETTLE_MODE, m);
+        return this;
+    }
+
+    public TransferMatcher withState(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.STATE, m);
+        return this;
+    }
+
+    public TransferMatcher withResume(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.RESUME, m);
+        return this;
+    }
+
+    public TransferMatcher withAborted(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.ABORTED, m);
+        return this;
+    }
+
+    public TransferMatcher withBatchable(Matcher<?> m) {
+        addFieldMatcher(Transfer.Field.BATCHABLE, m);
+        return this;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/TransferPayloadCompositeMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/TransferPayloadCompositeMatcher.java
new file mode 100644
index 0000000..4a82da5
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/transport/TransferPayloadCompositeMatcher.java
@@ -0,0 +1,292 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.transport;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.ApplicationPropertiesMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.DeliveryAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.FooterMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.HeaderMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.MessageAnnotationsMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.PropertiesMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.StringDescription;
+import org.hamcrest.TypeSafeMatcher;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Used to verify the Transfer frame payload, i.e the sections of the AMQP
+ * message such as the header, properties, and body sections.
+ */
+public class TransferPayloadCompositeMatcher extends TypeSafeMatcher<ByteBuf> {
+
+    private HeaderMatcher headersMatcher;
+    private String headerMatcherFailureDescription;
+    private DeliveryAnnotationsMatcher deliveryAnnotationsMatcher;
+    private String deliveryAnnotationsMatcherFailureDescription;
+    private MessageAnnotationsMatcher messageAnnotationsMatcher;
+    private String messageAnnotationsMatcherFailureDescription;
+    private PropertiesMatcher propertiesMatcher;
+    private String propertiesMatcherFailureDescription;
+    private ApplicationPropertiesMatcher applicationPropertiesMatcher;
+    private String applicationPropertiesMatcherFailureDescription;
+    private List<Matcher<ByteBuf>> msgContentMatchers = new ArrayList<>();
+    private String msgContentMatcherFailureDescription;
+    private FooterMatcher footersMatcher;
+    private String footerMatcherFailureDescription;
+    private Matcher<Integer> payloadLengthMatcher;
+    private String payloadLenthMatcherFailureDescription;
+
+    public TransferPayloadCompositeMatcher() {
+    }
+
+    @Override
+    protected boolean matchesSafely(final ByteBuf receivedBinary) {
+        int origLength = receivedBinary.readableBytes();
+        int bytesConsumed = 0;
+
+        // Length Matcher
+        if (payloadLengthMatcher != null) {
+            try {
+                assertThat("Payload length should match", origLength, payloadLengthMatcher);
+            } catch (Throwable t) {
+                payloadLenthMatcherFailureDescription = "\nPayload Lenfth Matcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        // MessageHeader Section
+        if (headersMatcher != null) {
+            ByteBuf msgHeaderEtcSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+            try {
+                bytesConsumed += headersMatcher.verify(msgHeaderEtcSubBinary);
+            } catch (Throwable t) {
+                headerMatcherFailureDescription = "\nActual encoded form of remaining bytes passed to MessageHeaderMatcher: " + msgHeaderEtcSubBinary;
+                headerMatcherFailureDescription += "\nMessageHeaderMatcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        // DeliveryAnnotations Section
+        if (deliveryAnnotationsMatcher != null) {
+            ByteBuf daAnnotationsEtcSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+            try {
+                bytesConsumed += deliveryAnnotationsMatcher.verify(daAnnotationsEtcSubBinary);
+            } catch (Throwable t) {
+                deliveryAnnotationsMatcherFailureDescription = "\nActual encoded form of remaining bytes passed to DeliveryAnnotationsMatcher: "
+                    + daAnnotationsEtcSubBinary;
+                deliveryAnnotationsMatcherFailureDescription += "\nDeliveryAnnotationsMatcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        // MessageAnnotations Section
+        if (messageAnnotationsMatcher != null) {
+            ByteBuf msgAnnotationsEtcSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+            try {
+                bytesConsumed += messageAnnotationsMatcher.verify(msgAnnotationsEtcSubBinary);
+            } catch (Throwable t) {
+                messageAnnotationsMatcherFailureDescription = "\nActual encoded form of remaining bytes passed to MessageAnnotationsMatcher: "
+                    + msgAnnotationsEtcSubBinary;
+                messageAnnotationsMatcherFailureDescription += "\nMessageAnnotationsMatcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        // Properties Section
+        if (propertiesMatcher != null) {
+            ByteBuf propsEtcSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+            try {
+                bytesConsumed += propertiesMatcher.verify(propsEtcSubBinary);
+            } catch (Throwable t) {
+                propertiesMatcherFailureDescription = "\nActual encoded form of remaining bytes passed to PropertiesMatcher: " + propsEtcSubBinary;
+                propertiesMatcherFailureDescription += "\nPropertiesMatcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        // Application Properties Section
+        if (applicationPropertiesMatcher != null) {
+            ByteBuf appPropsEtcSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+            try {
+                bytesConsumed += applicationPropertiesMatcher.verify(appPropsEtcSubBinary);
+            } catch (Throwable t) {
+                applicationPropertiesMatcherFailureDescription = "\nActual encoded form of remaining bytes passed to ApplicationPropertiesMatcher: " + appPropsEtcSubBinary;
+                applicationPropertiesMatcherFailureDescription += "\nApplicationPropertiesMatcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        // Message Content Body Section, already a Matcher<Binary>
+        if (!msgContentMatchers.isEmpty()) {
+            for (Matcher<ByteBuf> msgContentMatcher : msgContentMatchers) {
+                final ByteBuf msgContentBodyEtcSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+                final int originalReadableBytes = msgContentBodyEtcSubBinary.readableBytes();
+                final boolean contentMatches = msgContentMatcher.matches(msgContentBodyEtcSubBinary);
+                if (!contentMatches) {
+                    Description desc = new StringDescription();
+                    msgContentMatcher.describeTo(desc);
+                    msgContentMatcher.describeMismatch(msgContentBodyEtcSubBinary, desc);
+
+                    msgContentMatcherFailureDescription = "\nMessageContentMatcher mismatch Description:";
+                    msgContentMatcherFailureDescription += desc.toString();
+
+                    return false;
+                }
+
+                bytesConsumed += originalReadableBytes - msgContentBodyEtcSubBinary.readableBytes();
+            }
+        }
+
+        // MessageAnnotations Section
+        if (footersMatcher != null) {
+            ByteBuf footersSubBinary = receivedBinary.slice(bytesConsumed, origLength - bytesConsumed);
+            try {
+                bytesConsumed += footersMatcher.verify(footersSubBinary);
+            } catch (Throwable t) {
+                footerMatcherFailureDescription = "\nActual encoded form of remaining bytes passed to FooterMatcher: "
+                    + footersSubBinary;
+                footerMatcherFailureDescription += "\nFooterMatcher generated throwable: " + t;
+
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("a Binary encoding of a Transfer frames payload, containing an AMQP message");
+    }
+
+    @Override
+    protected void describeMismatchSafely(ByteBuf item, Description mismatchDescription) {
+        mismatchDescription.appendText("\nActual encoded form of the full Transfer frame payload: ").appendValue(item);
+
+        // Payload Length
+        if (payloadLenthMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nPayloadLengthMatcherFailed!");
+            mismatchDescription.appendText(payloadLenthMatcherFailureDescription);
+            return;
+        }
+
+        // MessageHeaders Section
+        if (headerMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nMessageHeadersMatcherFailed!");
+            mismatchDescription.appendText(headerMatcherFailureDescription);
+            return;
+        }
+
+        // MessageHeaders Section
+        if (deliveryAnnotationsMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nDeliveryAnnotationsMatcherFailed!");
+            mismatchDescription.appendText(deliveryAnnotationsMatcherFailureDescription);
+            return;
+        }
+
+        // MessageAnnotations Section
+        if (messageAnnotationsMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nMessageAnnotationsMatcherFailed!");
+            mismatchDescription.appendText(messageAnnotationsMatcherFailureDescription);
+            return;
+        }
+
+        // Properties Section
+        if (propertiesMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nPropertiesMatcherFailed!");
+            mismatchDescription.appendText(propertiesMatcherFailureDescription);
+            return;
+        }
+
+        // Application Properties Section
+        if (applicationPropertiesMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nApplicationPropertiesMatcherFailed!");
+            mismatchDescription.appendText(applicationPropertiesMatcherFailureDescription);
+            return;
+        }
+
+        // Message Content Body Section
+        if (msgContentMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nContentMatcherFailed!");
+            mismatchDescription.appendText(msgContentMatcherFailureDescription);
+            return;
+        }
+
+        // Footer Section
+        if (footerMatcherFailureDescription != null) {
+            mismatchDescription.appendText("\nContentMatcherFailed!");
+            mismatchDescription.appendText(footerMatcherFailureDescription);
+        }
+    }
+
+    public void setHeadersMatcher(HeaderMatcher headersMatcher) {
+        this.headersMatcher = headersMatcher;
+    }
+
+    public void setDeliveryAnnotationsMatcher(DeliveryAnnotationsMatcher deliveryAnnotationsMatcher) {
+        this.deliveryAnnotationsMatcher = deliveryAnnotationsMatcher;
+    }
+
+    public void setMessageAnnotationsMatcher(MessageAnnotationsMatcher msgAnnotationsMatcher) {
+        this.messageAnnotationsMatcher = msgAnnotationsMatcher;
+    }
+
+    public void setPropertiesMatcher(PropertiesMatcher propsMatcher) {
+        this.propertiesMatcher = propsMatcher;
+    }
+
+    public void setApplicationPropertiesMatcher(ApplicationPropertiesMatcher appPropsMatcher) {
+        this.applicationPropertiesMatcher = appPropsMatcher;
+    }
+
+    public void setMessageContentMatcher(Matcher<ByteBuf> msgContentMatcher) {
+        if (msgContentMatchers.isEmpty()) {
+            msgContentMatchers.add(msgContentMatcher);
+        } else {
+            msgContentMatchers.set(0, msgContentMatcher);
+        }
+    }
+
+    public void addMessageContentMatcher(Matcher<ByteBuf> msgContentMatcher) {
+        msgContentMatchers.add(msgContentMatcher);
+    }
+
+    public void setFootersMatcher(FooterMatcher footersMatcher) {
+        this.footersMatcher = footersMatcher;
+    }
+
+    public void setPayloadLengthMatcher(Matcher<Integer> payloadLengthMatcher) {
+        this.payloadLengthMatcher = payloadLengthMatcher;
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpSequenceMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpSequenceMatcher.java
new file mode 100644
index 0000000..b34f838
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpSequenceMatcher.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+
+public class EncodedAmqpSequenceMatcher extends EncodedAmqpTypeMatcher {
+
+    private static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:amqp-sequence:list");
+    private static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000076L);
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link AmqpSequence}
+     */
+    public EncodedAmqpSequenceMatcher(List<?> expectedValue) {
+        this(expectedValue, false);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link AmqpSequence}
+     * @param permitTrailingBytes
+     *        if it is permitted for bytes to be left in the Binary after
+     *        consuming the {@link AmqpSequence}
+     */
+    public EncodedAmqpSequenceMatcher(Object expectedValue, boolean permitTrailingBytes) {
+        super(DESCRIPTOR_SYMBOL, DESCRIPTOR_CODE, expectedValue, permitTrailingBytes);
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("a Binary encoding of an AmqpSequence that wraps: ").appendValue(getExpectedValue());
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpTypeMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpTypeMatcher.java
new file mode 100644
index 0000000..f4ead57
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpTypeMatcher.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.Codec;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+import io.netty.buffer.ByteBuf;
+
+public abstract class EncodedAmqpTypeMatcher extends TypeSafeMatcher<ByteBuf> {
+
+    private final Symbol descriptorSymbol;
+    private final UnsignedLong descriptorCode;
+    private final Object expectedValue;
+    private boolean permitTrailingBytes;
+    private DescribedType decodedDescribedType;
+    private boolean unexpectedTrailingBytes;
+
+    public EncodedAmqpTypeMatcher(Symbol symbol, UnsignedLong code, Object expectedValue) {
+        this(symbol, code, expectedValue, false);
+    }
+
+    public EncodedAmqpTypeMatcher(Symbol symbol, UnsignedLong code, Object expectedValue, boolean permitTrailingBytes) {
+        this.descriptorSymbol = symbol;
+        this.descriptorCode = code;
+        this.expectedValue = expectedValue;
+        this.permitTrailingBytes = permitTrailingBytes;
+    }
+
+    protected Object getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    protected boolean matchesSafely(ByteBuf receivedBinary) {
+        int length = receivedBinary.readableBytes();
+        Codec data = Codec.Factory.create();
+        long decoded = data.decode(receivedBinary);
+        decodedDescribedType = data.getDescribedType();
+        Object descriptor = decodedDescribedType.getDescriptor();
+
+        if (!(descriptorCode.equals(descriptor) || descriptorSymbol.equals(descriptor))) {
+            return false;
+        }
+
+        if (expectedValue == null && decodedDescribedType.getDescribed() != null) {
+            return false;
+        } else if (expectedValue != null) {
+            if (expectedValue instanceof Matcher) {
+                Matcher<?> matcher = (Matcher<?>) expectedValue;
+                if (!matcher.matches(decodedDescribedType.getDescribed())) {
+                    return false;
+                }
+            } else if (!expectedValue.equals(decodedDescribedType.getDescribed())) {
+                return false;
+            }
+        }
+
+        if (decoded < length && !permitTrailingBytes) {
+            unexpectedTrailingBytes = true;
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    protected void describeMismatchSafely(ByteBuf item, Description mismatchDescription) {
+        mismatchDescription.appendText("\nActual encoded form: ").appendValue(item);
+
+        if (decodedDescribedType != null) {
+            mismatchDescription.appendText("\nExpected descriptor: ")
+                               .appendValue(descriptorSymbol)
+                               .appendText(" / ")
+                               .appendValue(descriptorCode);
+            mismatchDescription.appendText("\nActual described type: ").appendValue(decodedDescribedType);
+        }
+
+        if (unexpectedTrailingBytes) {
+            mismatchDescription.appendText("\nUnexpected trailing bytes in provided bytes after decoding!");
+        }
+    }
+
+    /**
+     * Provide a description of this matcher.
+     */
+    @Override
+    public abstract void describeTo(Description description);
+
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpValueMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpValueMatcher.java
new file mode 100644
index 0000000..7819da1
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedAmqpValueMatcher.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpValue;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+
+public class EncodedAmqpValueMatcher extends EncodedAmqpTypeMatcher {
+
+    private static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:amqp-value:*");
+    private static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000077L);
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received {@link AmqpValue}
+     */
+    public EncodedAmqpValueMatcher(Object expectedValue) {
+        this(expectedValue, false);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received {@link AmqpValue}
+     * @param permitTrailingBytes
+     *        if it is permitted for bytes to be left in the Binary after
+     *        consuming the {@link AmqpValue}
+     */
+    public EncodedAmqpValueMatcher(Object expectedValue, boolean permitTrailingBytes) {
+        super(DESCRIPTOR_SYMBOL, DESCRIPTOR_CODE, expectedValue, permitTrailingBytes);
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("a Binary encoding of an AmqpValue that wraps: ").appendValue(getExpectedValue());
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedCompositingDataSectionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedCompositingDataSectionMatcher.java
new file mode 100644
index 0000000..d7862d8
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedCompositingDataSectionMatcher.java
@@ -0,0 +1,235 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import java.util.Objects;
+
+import org.apache.qpid.protonj2.test.driver.codec.EncodingCodes;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Data Section matcher that can be used with multiple expectTransfer calls to match a larger
+ * given payload block to the contents of one or more incoming Data sections split across multiple
+ * transfer frames and or multiple Data Sections within those transfer frames.
+ */
+public class EncodedCompositingDataSectionMatcher extends TypeSafeMatcher<ByteBuf> {
+
+    private static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:data:binary");
+    private static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000075L);
+
+    private final int expectedValueSize;
+    private final ByteBuf expectedValue;
+
+    private boolean expectTrailingBytes;
+    private String decodingErrorDescription;
+
+    // State data used during validation of the composite data values
+    private boolean unexpectedTrailingBytes;
+    private boolean expectDataSectionPreamble = true;
+    private int expectedCurrentDataSectionBytes = -1;
+    private int expectedRemainingBytes;
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedCompositingDataSectionMatcher(byte[] expectedValue) {
+        this(Unpooled.wrappedBuffer(expectedValue));
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedCompositingDataSectionMatcher(Binary expectedValue) {
+        this(Unpooled.wrappedBuffer(expectedValue.asByteBuffer()));
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedCompositingDataSectionMatcher(ByteBuf expectedValue) {
+        Objects.requireNonNull(expectedValue, "The expected value cannot be null for this matcher");
+
+        this.expectedValue = expectedValue;
+        this.expectedRemainingBytes = this.expectedValue.readableBytes();
+        this.expectedValueSize = this.expectedRemainingBytes;
+    }
+
+    public boolean isTrailingBytesExpected() {
+        return expectTrailingBytes;
+    }
+
+    public EncodedCompositingDataSectionMatcher setExpectTrailingBytes(boolean expectTrailingBytes) {
+        this.expectTrailingBytes = expectTrailingBytes;
+        return this;
+    }
+
+    protected Object getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    protected boolean matchesSafely(ByteBuf receivedBinary) {
+        if (expectDataSectionPreamble) {
+            Object descriptor = readDescribedTypeEncoding(receivedBinary);
+
+            if (!(DESCRIPTOR_CODE.equals(descriptor) || DESCRIPTOR_SYMBOL.equals(descriptor))) {
+                return false;
+            }
+
+            // Should be a Binary AMQP type with a length value and possibly some bytes
+            byte encodingCode = receivedBinary.readByte();
+
+            if (encodingCode == EncodingCodes.VBIN8) {
+                expectedCurrentDataSectionBytes = receivedBinary.readByte() & 0xFF;
+            } else if (encodingCode == EncodingCodes.VBIN32) {
+                expectedCurrentDataSectionBytes = receivedBinary.readInt();
+            } else {
+                decodingErrorDescription = "Expceted to read a Binary Type but read encoding code: " + encodingCode;
+                return false;
+            }
+
+            if (expectedCurrentDataSectionBytes > expectedRemainingBytes) {
+                decodingErrorDescription = "Expceted encoded Binary to indicate size of: " + expectedRemainingBytes + ", " +
+                                           "or less but read an encoded size of: " + expectedCurrentDataSectionBytes;
+                return false;
+            }
+
+            expectDataSectionPreamble = false;  // We got the current preamble
+        }
+
+        if (expectedRemainingBytes != 0) {
+            final int currentChunkSize = Math.min(expectedCurrentDataSectionBytes, receivedBinary.readableBytes());
+            final ByteBuf expectedValueChunk = expectedValue.slice(expectedValue.readerIndex(), currentChunkSize);
+            final ByteBuf currentChunk = receivedBinary.slice(receivedBinary.readerIndex(), currentChunkSize);
+
+            receivedBinary.skipBytes(currentChunkSize);
+            expectedValue.skipBytes(currentChunkSize);
+
+            if (!expectedValueChunk.equals(currentChunk)) {
+                return false;
+            }
+
+            expectedRemainingBytes -= currentChunkSize;
+            expectedCurrentDataSectionBytes -= currentChunkSize;
+
+            if (expectedRemainingBytes != 0 && expectedCurrentDataSectionBytes == 0) {
+                expectDataSectionPreamble = true;
+                expectedCurrentDataSectionBytes = -1;
+            }
+        }
+
+        if (expectedRemainingBytes == 0 && receivedBinary.isReadable() && !isTrailingBytesExpected()) {
+            unexpectedTrailingBytes = true;
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private static final int DESCRIBED_TYPE_INDICATOR = 0;
+
+    private Object readDescribedTypeEncoding(ByteBuf data) {
+        byte encodingCode = data.readByte();
+
+        if (encodingCode == DESCRIBED_TYPE_INDICATOR) {
+            encodingCode = data.readByte();
+            switch (encodingCode) {
+                case EncodingCodes.ULONG0:
+                    return UnsignedLong.ZERO;
+                case EncodingCodes.SMALLULONG:
+                    return UnsignedLong.valueOf(data.readByte() & 0xff);
+                case EncodingCodes.ULONG:
+                    return UnsignedLong.valueOf(data.readLong());
+                case EncodingCodes.SYM8:
+                    return readSymbol8(data);
+                case EncodingCodes.SYM32:
+                    return readSymbol32(data);
+                default:
+                    decodingErrorDescription = "Expected Unsigned Long or Symbol type but found encoding: " +  encodingCode;
+            }
+        } else {
+            decodingErrorDescription = "Expceted to read a Described Type but read encoding code: " + encodingCode;
+        }
+
+        return null;
+    }
+
+    private Symbol readSymbol32(ByteBuf buffer) {
+        int length = buffer.readInt();
+
+        if (length == 0) {
+            return Symbol.valueOf("");
+        } else {
+            ByteBuf symbolBuffer = buffer.slice(buffer.readerIndex(), length);
+            buffer.skipBytes(length);
+
+            return Symbol.getSymbol(symbolBuffer.nioBuffer(), true);
+        }
+    }
+
+    private Symbol readSymbol8(ByteBuf buffer) {
+        int length = buffer.readByte() & 0xFF;
+
+        if (length == 0) {
+            return Symbol.valueOf("");
+        } else {
+            ByteBuf symbolBuffer = buffer.slice(buffer.readerIndex(), length);
+            buffer.skipBytes(length);
+
+            return Symbol.getSymbol(symbolBuffer.nioBuffer(), true);
+        }
+    }
+
+    @Override
+    protected void describeMismatchSafely(ByteBuf item, Description mismatchDescription) {
+        mismatchDescription.appendText("\nActual encoded form: ").appendValue(item);
+
+        if (decodingErrorDescription != null) {
+            mismatchDescription.appendText("\nExpected descriptor: ")
+                               .appendValue(DESCRIPTOR_SYMBOL)
+                               .appendText(" / ")
+                               .appendValue(DESCRIPTOR_CODE);
+            mismatchDescription.appendText("\nError that failed the validation: ").appendValue(decodingErrorDescription);
+        }
+
+        if (unexpectedTrailingBytes) {
+            mismatchDescription.appendText("\nUnexpected trailing bytes in provided bytes after decoding!");
+        }
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("a complete Binary encoding of a Data section that wraps")
+                   .appendText(" an collection of bytes of eventual size {").appendValue(expectedValueSize)
+                   .appendText("}").appendText(" containing: ").appendValue(getExpectedValue());
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedDataMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedDataMatcher.java
new file mode 100644
index 0000000..77ea14e
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedDataMatcher.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpValue;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+
+public class EncodedDataMatcher extends EncodedAmqpTypeMatcher {
+
+    private static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:data:binary");
+    private static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000075L);
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedDataMatcher(byte[] expectedValue) {
+        this(new Binary(expectedValue), false);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedDataMatcher(Binary expectedValue) {
+        this(expectedValue, false);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     * @param permitTrailingBytes
+     *        if it is permitted for bytes to be left in the Binary after
+     *        consuming the {@link AmqpValue}
+     */
+    public EncodedDataMatcher(byte[] expectedValue, boolean permitTrailingBytes) {
+        this(new Binary(expectedValue), permitTrailingBytes);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     * @param permitTrailingBytes
+     *        if it is permitted for bytes to be left in the Binary after
+     *        consuming the {@link AmqpValue}
+     */
+    public EncodedDataMatcher(Binary expectedValue, boolean permitTrailingBytes) {
+        super(DESCRIPTOR_SYMBOL, DESCRIPTOR_CODE, expectedValue, permitTrailingBytes);
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("a Binary encoding of a Data that wraps a Binary containing: ").appendValue(getExpectedValue());
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedPartialDataSectionMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedPartialDataSectionMatcher.java
new file mode 100644
index 0000000..a94e364
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/EncodedPartialDataSectionMatcher.java
@@ -0,0 +1,258 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.EncodingCodes;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+public class EncodedPartialDataSectionMatcher extends TypeSafeMatcher<ByteBuf> {
+
+    private static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:data:binary");
+    private static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000075L);
+
+    private final boolean expectDataSectionPreamble;
+    private final ByteBuf expectedValue;
+    private final int expectedEncodedSize;
+    private boolean expectTrailingBytes;
+    private String decodingErrorDescription;
+    private boolean unexpectedTrailingBytes;
+
+    /**
+     * @param expectedEncodedSize
+     *        the actual encoded size the Data section binary should eventually
+     *        receive once all split frame transfers have arrived.
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedPartialDataSectionMatcher(int expectedEncodedSize, byte[] expectedValue) {
+        this(expectedEncodedSize, Unpooled.wrappedBuffer(expectedValue), true);
+    }
+
+    /**
+     * @param expectedEncodedSize
+     *        the actual encoded size the Data section binary should eventually
+     *        receive once all split frame transfers have arrived.
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedPartialDataSectionMatcher(int expectedEncodedSize, Binary expectedValue) {
+        this(expectedEncodedSize, Unpooled.wrappedBuffer(expectedValue.asByteBuffer()), true);
+    }
+
+    /**
+     * @param expectedEncodedSize
+     *        the actual encoded size the Data section binary should eventually
+     *        receive once all split frame transfers have arrived.
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedPartialDataSectionMatcher(int expectedEncodedSize, ByteBuf expectedValue) {
+        this(expectedEncodedSize, expectedValue, true);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedPartialDataSectionMatcher(byte[] expectedValue) {
+        this(-1, Unpooled.wrappedBuffer(expectedValue), false);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedPartialDataSectionMatcher(Binary expectedValue) {
+        this(-1, Unpooled.wrappedBuffer(expectedValue.asByteBuffer()), false);
+    }
+
+    /**
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     */
+    public EncodedPartialDataSectionMatcher(ByteBuf expectedValue) {
+        this(-1, expectedValue, false);
+    }
+
+    /**
+     * @param expectedEncodedSize
+     *        the actual encoded size the Data section binary should eventually
+     *        receive once all split frame transfers have arrived.
+     * @param expectedValue
+     *        the value that is expected to be IN the received
+     *        {@link org.apache.qpid.proton.amqp.messaging.Data}
+     * @param expectDataSectionPreamble
+     *        should the matcher check for the Data and Binary section encoding
+     *        meta-data or only match the payload to the given expected value.
+     */
+    protected EncodedPartialDataSectionMatcher(int expectedEncodedSize, ByteBuf expectedValue, boolean expectDataSectionPreamble) {
+        this.expectedValue = expectedValue;
+        this.expectedEncodedSize = expectedEncodedSize;
+        this.expectDataSectionPreamble = expectDataSectionPreamble;
+    }
+
+    public boolean isTrailingBytesExpected() {
+        return expectTrailingBytes;
+    }
+
+    public EncodedPartialDataSectionMatcher setExpectTrailingBytes(boolean expectTrailingBytes) {
+        this.expectTrailingBytes = expectTrailingBytes;
+        return this;
+    }
+
+    protected Object getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    protected boolean matchesSafely(ByteBuf receivedBinary) {
+        if (expectDataSectionPreamble) {
+            Object descriptor = readDescribedTypeEncoding(receivedBinary);
+
+            if (!(DESCRIPTOR_CODE.equals(descriptor) || DESCRIPTOR_SYMBOL.equals(descriptor))) {
+                return false;
+            }
+
+            // Should be a Binary AMQP type with a length value and possibly some bytes
+            byte encodingCode = receivedBinary.readByte();
+            int binaryEncodedSize = -1;
+
+            if (encodingCode == EncodingCodes.VBIN8) {
+                binaryEncodedSize = receivedBinary.readByte() & 0xFF;
+            } else if (encodingCode == EncodingCodes.VBIN32) {
+                binaryEncodedSize = receivedBinary.readInt();
+            } else {
+                decodingErrorDescription = "Expceted to read a Binary Type but read encoding code: " + encodingCode;
+                return false;
+            }
+
+            if (binaryEncodedSize != expectedEncodedSize) {
+                decodingErrorDescription = "Expceted encoded Binary to indicate size of: " + expectedEncodedSize + ", " +
+                                           "but read an encoded size of: " + binaryEncodedSize;
+                return false;
+            }
+        }
+
+        if (expectedValue != null) {
+            ByteBuf payload = receivedBinary.slice();
+            receivedBinary.skipBytes(payload.readableBytes());
+            if (!expectedValue.equals(payload)) {
+                return false;
+            }
+        }
+
+        if (receivedBinary.isReadable() && !isTrailingBytesExpected()) {
+            unexpectedTrailingBytes = true;
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private static final int DESCRIBED_TYPE_INDICATOR = 0;
+
+    private Object readDescribedTypeEncoding(ByteBuf data) {
+        byte encodingCode = data.readByte();
+
+        if (encodingCode == DESCRIBED_TYPE_INDICATOR) {
+            encodingCode = data.readByte();
+            switch (encodingCode) {
+                case EncodingCodes.ULONG0:
+                    return UnsignedLong.ZERO;
+                case EncodingCodes.SMALLULONG:
+                    return UnsignedLong.valueOf(data.readByte() & 0xff);
+                case EncodingCodes.ULONG:
+                    return UnsignedLong.valueOf(data.readLong());
+                case EncodingCodes.SYM8:
+                    return readSymbol8(data);
+                case EncodingCodes.SYM32:
+                    return readSymbol32(data);
+                default:
+                    decodingErrorDescription = "Expected Unsigned Long or Symbol type but found encoding: " +  encodingCode;
+            }
+        } else {
+            decodingErrorDescription = "Expceted to read a Described Type but read encoding code: " + encodingCode;
+        }
+
+        return null;
+    }
+
+    private Symbol readSymbol32(ByteBuf buffer) {
+        int length = buffer.readInt();
+
+        if (length == 0) {
+            return Symbol.valueOf("");
+        } else {
+            ByteBuf symbolBuffer = buffer.slice(buffer.readerIndex(), length);
+            buffer.skipBytes(length);
+
+            return Symbol.getSymbol(symbolBuffer.nioBuffer(), true);
+        }
+    }
+
+    private Symbol readSymbol8(ByteBuf buffer) {
+        int length = buffer.readByte() & 0xFF;
+
+        if (length == 0) {
+            return Symbol.valueOf("");
+        } else {
+            ByteBuf symbolBuffer = buffer.slice(buffer.readerIndex(), length);
+            buffer.skipBytes(length);
+
+            return Symbol.getSymbol(symbolBuffer.nioBuffer(), true);
+        }
+    }
+
+    @Override
+    protected void describeMismatchSafely(ByteBuf item, Description mismatchDescription) {
+        mismatchDescription.appendText("\nActual encoded form: ").appendValue(item);
+
+        if (decodingErrorDescription != null) {
+            mismatchDescription.appendText("\nExpected descriptor: ")
+                               .appendValue(DESCRIPTOR_SYMBOL)
+                               .appendText(" / ")
+                               .appendValue(DESCRIPTOR_CODE);
+            mismatchDescription.appendText("\nError that failed the validation: ").appendValue(decodingErrorDescription);
+        }
+
+        if (unexpectedTrailingBytes) {
+            mismatchDescription.appendText("\nUnexpected trailing bytes in provided bytes after decoding!");
+        }
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("a partial Binary encoding of a Data section that wraps")
+                   .appendText(" an incomplete Binary of eventual size {").appendValue(expectedEncodedSize)
+                   .appendText("}").appendText(" containing: ").appendValue(getExpectedValue());
+    }
+}
\ No newline at end of file
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedByteMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedByteMatcher.java
new file mode 100644
index 0000000..e85d365
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedByteMatcher.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Matcher for values that must decode to an AMQP UnsignedInteger
+ */
+public class UnsignedByteMatcher extends TypeSafeMatcher<UnsignedByte> {
+
+    private final UnsignedByte expectedValue;
+
+    public UnsignedByteMatcher(byte expectedValue) {
+        this.expectedValue = UnsignedByte.valueOf(expectedValue);
+    }
+
+    public UnsignedByteMatcher(UnsignedByte expectedValue) {
+        this.expectedValue = expectedValue;
+    }
+
+    protected UnsignedByte getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("Expected UnsignedByte:{")
+                   .appendValue(expectedValue)
+                   .appendText("}");
+    }
+
+    @Override
+    protected void describeMismatchSafely(UnsignedByte item, Description mismatchDescription) {
+        mismatchDescription.appendText("Actual value received:{")
+                           .appendValue(item)
+                           .appendText("}");
+    }
+
+    @Override
+    protected boolean matchesSafely(UnsignedByte item) {
+        if (expectedValue == null) {
+            return item == null;
+        } else {
+            return expectedValue.equals(item);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedIntegerMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedIntegerMatcher.java
new file mode 100644
index 0000000..2dbaa27
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedIntegerMatcher.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Matcher for values that must decode to an AMQP UnsignedInteger
+ */
+public class UnsignedIntegerMatcher extends TypeSafeMatcher<UnsignedInteger> {
+
+    private final UnsignedInteger expectedValue;
+
+    public UnsignedIntegerMatcher(int expectedValue) {
+        this.expectedValue = UnsignedInteger.valueOf(expectedValue);
+    }
+
+    public UnsignedIntegerMatcher(long expectedValue) {
+        this.expectedValue = UnsignedInteger.valueOf(expectedValue);
+    }
+
+    public UnsignedIntegerMatcher(UnsignedInteger expectedValue) {
+        this.expectedValue = expectedValue;
+    }
+
+    protected UnsignedInteger getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("Expected UnsignedInteger:{")
+                   .appendValue(expectedValue)
+                   .appendText("}");
+    }
+
+    @Override
+    protected void describeMismatchSafely(UnsignedInteger item, Description mismatchDescription) {
+        mismatchDescription.appendText("Actual value received:{")
+                           .appendValue(item)
+                           .appendText("}");
+    }
+
+    @Override
+    protected boolean matchesSafely(UnsignedInteger item) {
+        if (expectedValue == null) {
+            return item == null;
+        } else {
+            return expectedValue.equals(item);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedLongMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedLongMatcher.java
new file mode 100644
index 0000000..d710126
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedLongMatcher.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedLong;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Matcher for values that must decode to an AMQP UnsignedInteger
+ */
+public class UnsignedLongMatcher extends TypeSafeMatcher<UnsignedLong> {
+
+    private final UnsignedLong expectedValue;
+
+    public UnsignedLongMatcher(int expectedValue) {
+        this.expectedValue = UnsignedLong.valueOf(expectedValue);
+    }
+
+    public UnsignedLongMatcher(long expectedValue) {
+        this.expectedValue = UnsignedLong.valueOf(expectedValue);
+    }
+
+    public UnsignedLongMatcher(UnsignedLong expectedValue) {
+        this.expectedValue = expectedValue;
+    }
+
+    protected UnsignedLong getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("Expected UnsignedLong:{")
+                   .appendValue(expectedValue)
+                   .appendText("}");
+    }
+
+    @Override
+    protected void describeMismatchSafely(UnsignedLong item, Description mismatchDescription) {
+        mismatchDescription.appendText("Actual value received:{")
+                           .appendValue(item)
+                           .appendText("}");
+    }
+
+    @Override
+    protected boolean matchesSafely(UnsignedLong item) {
+        if (expectedValue == null) {
+            return item == null;
+        } else {
+            return expectedValue.equals(item);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedShortMatcher.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedShortMatcher.java
new file mode 100644
index 0000000..2a84cd1
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/matchers/types/UnsignedShortMatcher.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matchers.types;
+
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Matcher for values that must decode to an AMQP UnsignedInteger
+ */
+public class UnsignedShortMatcher extends TypeSafeMatcher<UnsignedShort> {
+
+    private final UnsignedShort expectedValue;
+
+    public UnsignedShortMatcher(short expectedValue) {
+        this.expectedValue = UnsignedShort.valueOf(expectedValue);
+    }
+
+    public UnsignedShortMatcher(int expectedValue) {
+        this.expectedValue = UnsignedShort.valueOf(expectedValue);
+    }
+
+    public UnsignedShortMatcher(UnsignedShort expectedValue) {
+        this.expectedValue = expectedValue;
+    }
+
+    protected UnsignedShort getExpectedValue() {
+        return expectedValue;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("Expected UnsignedShort:{")
+                   .appendValue(expectedValue)
+                   .appendText("}");
+    }
+
+    @Override
+    protected void describeMismatchSafely(UnsignedShort item, Description mismatchDescription) {
+        mismatchDescription.appendText("Actual value received:{")
+                           .appendValue(item)
+                           .appendText("}");
+    }
+
+    @Override
+    protected boolean matchesSafely(UnsignedShort item) {
+        if (expectedValue == null) {
+            return item == null;
+        } else {
+            return expectedValue.equals(item);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/NettyClient.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/NettyClient.java
new file mode 100644
index 0000000..62cb9ef
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/NettyClient.java
@@ -0,0 +1,455 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.netty;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.test.driver.ProtonTestClientOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.FixedRecvByteBufAllocator;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketVersion;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.concurrent.ScheduledFuture;
+
+/**
+ * Self contained Netty client implementation that provides a base for more
+ * complex client implementations to use as the IO layer.
+ */
+public abstract class NettyClient implements AutoCloseable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NettyClient.class);
+
+    private static final String AMQP_SUB_PROTOCOL = "amqp";
+
+    private Bootstrap bootstrap;
+    private EventLoopGroup group;
+    private Channel channel;
+    private String host;
+    private int port;
+    protected volatile IOException failureCause;
+    private final ProtonTestClientOptions options;
+    private volatile SslHandler sslHandler;
+    protected final AtomicBoolean connected = new AtomicBoolean();
+    protected final AtomicBoolean closed = new AtomicBoolean();
+    protected final CountDownLatch connectedLatch = new CountDownLatch(1);
+
+    public NettyClient(ProtonTestClientOptions options) {
+        this.options = options;
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (closed.compareAndSet(false, true)) {
+            connected.set(false);
+            connectedLatch.countDown();
+            if (channel != null) {
+                try {
+                    if (!channel.close().await(10, TimeUnit.SECONDS)) {
+                        LOG.info("Channel close timed out wiating for result");
+                    }
+                } catch (InterruptedException e) {
+                    Thread.interrupted();
+                    LOG.debug("Close of channel interrupted while awaiting result");
+                }
+            }
+        }
+    }
+
+    public void connect(String host, int port) throws IOException {
+        if (closed.get()) {
+            throw new IllegalStateException("Netty client has already been closed");
+        }
+
+        if (host == null || host.isEmpty()) {
+            throw new IllegalArgumentException("Transport host value cannot be null");
+        }
+
+        this.host = host;
+
+        if (port > 0) {
+            this.port = port;
+        } else {
+            if (options.isSecure()) {
+                this.port = ProtonTestClientOptions.DEFAULT_SSL_PORT;
+            } else {
+                this.port = ProtonTestClientOptions.DEFAULT_TCP_PORT;
+            }
+        }
+
+        group = new NioEventLoopGroup(1);
+        bootstrap = new Bootstrap().channel(NioSocketChannel.class).group(group);
+        bootstrap.handler(new ChannelInitializer<Channel>() {
+            @Override
+            public void initChannel(Channel transportChannel) throws Exception {
+                channel = transportChannel;
+                configureChannel(transportChannel);
+            }
+        });
+
+        configureNetty(bootstrap, options);
+
+        bootstrap.connect(host, port).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        try {
+            connectedLatch.await();
+        } catch (InterruptedException e) {
+            Thread.interrupted();
+        }
+
+        if (!connected.get()) {
+            if (failureCause != null) {
+                throw failureCause;
+            } else {
+                throw new IOException("Netty client was closed before a connection was established.");
+            }
+        }
+    }
+
+    public EventLoop eventLoop() {
+        if (channel == null || !channel.isActive()) {
+            throw new IllegalStateException("Channel is not connected or has closed");
+        }
+
+        return channel.eventLoop();
+    }
+
+    public void write(ByteBuffer buffer) {
+        if (channel == null || !channel.isActive()) {
+            throw new IllegalStateException("Channel is not connected or has closed");
+        }
+
+        channel.writeAndFlush(Unpooled.wrappedBuffer(buffer), channel.voidPromise());
+    }
+
+    public boolean isConnected() {
+        return connected.get();
+    }
+
+    public boolean isSecure() {
+        return options.isSecure();
+    }
+
+    public URI getRemoteURI() {
+        if (host != null) {
+            try {
+                if (options.isUseWebSockets()) {
+                    return new URI(options.isSecure() ? "wss" : "ws", null, host, port, options.getWebSocketPath(), null, null);
+                } else {
+                    return new URI(options.isSecure() ? "ssl" : "tcp", null, host, port, null, null, null);
+                }
+            } catch (URISyntaxException e) {
+            }
+        }
+
+        return null;
+    }
+
+    //----- Default implementation of Netty handler
+
+    protected class NettyClientInboundHandler extends ChannelInboundHandlerAdapter {
+
+        private final WebSocketClientHandshaker handshaker;
+        private ScheduledFuture<?> handshakeTimeoutFuture;
+
+        public NettyClientInboundHandler() {
+            if (options.isUseWebSockets()) {
+                DefaultHttpHeaders headers = new DefaultHttpHeaders();
+
+                options.getHttpHeaders().forEach((key, value) -> {
+                    headers.set(key, value);
+                });
+
+                handshaker = WebSocketClientHandshakerFactory.newHandshaker(
+                    getRemoteURI(), WebSocketVersion.V13, AMQP_SUB_PROTOCOL,
+                    true, headers, options.getWebSocketMaxFrameSize());
+            } else {
+                handshaker = null;
+            }
+        }
+
+        @Override
+        public final void channelRegistered(ChannelHandlerContext context) throws Exception {
+            channel = context.channel();
+        }
+
+        @Override
+        public void channelActive(ChannelHandlerContext context) throws Exception {
+            if (options.isUseWebSockets()) {
+                handshaker.handshake(context.channel());
+
+                handshakeTimeoutFuture = context.executor().schedule(()-> {
+                    LOG.trace("WebSocket handshake timed out! Channel is {}", context.channel());
+                    if (!handshaker.isHandshakeComplete()) {
+                        NettyClient.this.handleTransportFailure(channel, new IOException("WebSocket handshake timed out"));
+                    }
+                }, options.getConnectTimeout(), TimeUnit.MILLISECONDS);
+            }
+
+            // In the Secure case we need to let the handshake complete before we
+            // trigger the connected event.
+            if (!isSecure()) {
+                if (!options.isUseWebSockets()) {
+                    handleConnected(context.channel());
+                }
+            } else {
+                SslHandler sslHandler = context.pipeline().get(SslHandler.class);
+                sslHandler.handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {
+                    @Override
+                    public void operationComplete(Future<Channel> future) throws Exception {
+                        if (future.isSuccess()) {
+                            LOG.trace("SSL Handshake has completed: {}", channel);
+                            if (!options.isUseWebSockets()) {
+                                handleConnected(channel);
+                            }
+                        } else {
+                            LOG.trace("SSL Handshake has failed: {}", channel);
+                            handleTransportFailure(channel, future.cause());
+                        }
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext context) throws Exception {
+            if (handshakeTimeoutFuture != null) {
+                handshakeTimeoutFuture.cancel(false);
+            }
+
+            handleTransportFailure(context.channel(), new IOException("Remote closed connection unexpectedly"));
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
+            handleTransportFailure(context.channel(), cause);
+        }
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object message) {
+            if (options.isUseWebSockets()) {
+                LOG.trace("New data read: incoming: {}", message);
+
+                Channel ch = ctx.channel();
+                if (!handshaker.isHandshakeComplete()) {
+                    handshaker.finishHandshake(ch, (FullHttpResponse) message);
+                    LOG.trace("WebSocket Client connected! {}", ctx.channel());
+                    // Now trigger super processing as we are really connected.
+                    if (handshakeTimeoutFuture.cancel(false)) {
+                        handleConnected(ch);
+                    }
+
+                    return;
+                }
+
+                // We shouldn't get this since we handle the handshake previously.
+                if (message instanceof FullHttpResponse) {
+                    FullHttpResponse response = (FullHttpResponse) message;
+                    throw new IllegalStateException(
+                        "Unexpected FullHttpResponse (getStatus=" + response.status() +
+                        ", content=" + response.content().toString(StandardCharsets.UTF_8) + ')');
+                }
+
+                WebSocketFrame frame = (WebSocketFrame) message;
+                if (frame instanceof TextWebSocketFrame) {
+                    TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
+                    LOG.warn("WebSocket Client received message: " + textFrame.text());
+                    ctx.fireExceptionCaught(new IOException("Received invalid frame over WebSocket."));
+                } else if (frame instanceof BinaryWebSocketFrame) {
+                    BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame;
+                    LOG.trace("WebSocket Client received data: {} bytes", binaryFrame.content().readableBytes());
+                    ctx.fireChannelRead(binaryFrame.content());
+                } else if (frame instanceof ContinuationWebSocketFrame) {
+                    ContinuationWebSocketFrame continuationFrame = (ContinuationWebSocketFrame) frame;
+                    LOG.trace("WebSocket Client received data continuation: {} bytes", continuationFrame.content().readableBytes());
+                    ctx.fireChannelRead(continuationFrame.content());
+                } else if (frame instanceof PingWebSocketFrame) {
+                    LOG.trace("WebSocket Client received ping, response with pong");
+                    ch.write(new PongWebSocketFrame(frame.content()));
+                } else if (frame instanceof CloseWebSocketFrame) {
+                    LOG.trace("WebSocket Client received closing");
+                    ch.close();
+                }
+            } else {
+                ctx.fireChannelRead(message);
+            }
+        }
+    }
+
+    private class NettyClientOutboundHandler extends ChannelOutboundHandlerAdapter  {
+
+        @Override
+        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+            LOG.trace("NettyServerHandler: Channel write: {}", msg);
+            if (options.isUseWebSockets() && msg instanceof ByteBuf) {
+                if (options.isFragmentWrites()) {
+                    ByteBuf orig = (ByteBuf) msg;
+                    int origIndex = orig.readerIndex();
+                    int split = orig.readableBytes()/2;
+
+                    ByteBuf part1 = orig.copy(origIndex, split);
+                    LOG.trace("NettyClientOutboundHandler: Part1: {}", part1);
+                    orig.readerIndex(origIndex + split);
+                    LOG.trace("NettyClientOutboundHandler: Part2: {}", orig);
+
+                    BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false, 0, part1);
+                    ctx.writeAndFlush(frame1);
+                    ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(true, 0, orig);
+                    ctx.write(frame2, promise);
+                } else {
+                    BinaryWebSocketFrame frame = new BinaryWebSocketFrame((ByteBuf) msg);
+                    ctx.write(frame, promise);
+                }
+            } else {
+                ctx.write(msg, promise);
+            }
+        }
+    }
+
+    //----- Internal Client implementation API
+
+    protected abstract ChannelHandler getClientHandler();
+
+    protected ScheduledExecutorService getEventLoop() {
+        if (channel == null || !channel.isActive()) {
+            throw new IllegalStateException("Channel is not connected or has closed");
+        }
+
+        return channel.eventLoop();
+    }
+
+    protected SslHandler getSslHandler() {
+        return sslHandler;
+    }
+
+    private void configureChannel(final Channel channel) throws Exception {
+        if (isSecure()) {
+            final SslHandler sslHandler;
+            try {
+                sslHandler = SslSupport.createClientSslHandler(getRemoteURI(), options);
+            } catch (Exception ex) {
+                LOG.warn("Error during initialization of channel from SSL Handler creation:");
+                handleTransportFailure(channel, ex);
+                throw new IOException(ex);
+            }
+
+            channel.pipeline().addLast("ssl", sslHandler);
+        }
+
+        if (options.isTraceBytes()) {
+            channel.pipeline().addLast("logger", new LoggingHandler(getClass()));
+        }
+
+        if (options.isUseWebSockets()) {
+            channel.pipeline().addLast(new HttpClientCodec());
+            channel.pipeline().addLast(new HttpObjectAggregator(8192));
+        }
+
+        channel.pipeline().addLast(new NettyClientOutboundHandler());
+        channel.pipeline().addLast(new NettyClientInboundHandler());
+        channel.pipeline().addLast(getClientHandler());
+    }
+
+    private void configureNetty(Bootstrap bootstrap, ProtonTestClientOptions options) {
+        bootstrap.option(ChannelOption.TCP_NODELAY, options.isTcpNoDelay());
+        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, options.getConnectTimeout());
+        bootstrap.option(ChannelOption.SO_KEEPALIVE, options.isTcpKeepAlive());
+        bootstrap.option(ChannelOption.SO_LINGER, options.getSoLinger());
+
+        if (options.getSendBufferSize() != -1) {
+            bootstrap.option(ChannelOption.SO_SNDBUF, options.getSendBufferSize());
+        }
+
+        if (options.getReceiveBufferSize() != -1) {
+            bootstrap.option(ChannelOption.SO_RCVBUF, options.getReceiveBufferSize());
+            bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(options.getReceiveBufferSize()));
+        }
+
+        if (options.getTrafficClass() != -1) {
+            bootstrap.option(ChannelOption.IP_TOS, options.getTrafficClass());
+        }
+
+        if (options.getLocalAddress() != null || options.getLocalPort() != 0) {
+            if (options.getLocalAddress() != null) {
+                bootstrap.localAddress(options.getLocalAddress(), options.getLocalPort());
+            } else {
+                bootstrap.localAddress(options.getLocalPort());
+            }
+        }
+    }
+
+    //----- Event Handlers which can be overridden in subclasses -------------//
+
+    protected void handleConnected(Channel connectedChannel) {
+        LOG.trace("Channel has become active! Channel is {}", connectedChannel);
+        channel = connectedChannel;
+        connected.set(true);
+        connectedLatch.countDown();
+    }
+
+    protected void handleTransportFailure(Channel failedChannel, Throwable cause) {
+        if (!closed.get()) {
+            LOG.trace("Channel indicates connection failure! Channel is {}", failedChannel);
+            failureCause = new IOException(cause);
+            channel = failedChannel;
+            connected.set(false);
+            connectedLatch.countDown();
+        } else {
+            LOG.trace("Closed Channel signalled that the channel ended: {}", channel);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/NettyServer.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/NettyServer.java
new file mode 100644
index 0000000..7ef2105
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/NettyServer.java
@@ -0,0 +1,420 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.netty;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+
+/**
+ * Base Server implementation used to create Netty based server implementations for
+ * unit testing aspects of the client code.
+ */
+public abstract class NettyServer implements AutoCloseable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NettyServer.class);
+
+    static final int PORT = Integer.parseInt(System.getProperty("port", "5672"));
+    static final String WEBSOCKET_PATH = "/";
+    static final int DEFAULT_MAX_FRAME_SIZE = 65535;
+
+    private EventLoopGroup bossGroup;
+    private EventLoopGroup workerGroup;
+    private Channel serverChannel;
+    private Channel clientChannel;
+    private final ProtonTestServerOptions options;
+    private int maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
+    private String webSocketPath = WEBSOCKET_PATH;
+    private volatile SslHandler sslHandler;
+    private volatile HandshakeComplete handshakeComplete;
+    private final CountDownLatch handshakeCompletion = new CountDownLatch(1);
+
+    private final AtomicBoolean started = new AtomicBoolean();
+
+    public NettyServer(ProtonTestServerOptions options) {
+        this.options = options;
+    }
+
+    public boolean isSecureServer() {
+        return options.isSecure();
+    }
+
+    public boolean isAcceptingConnections() {
+        return serverChannel != null && serverChannel.isOpen();
+    }
+
+    public boolean hasSecureConnection() {
+        return sslHandler != null;
+    }
+
+    public boolean hasClientConnection() {
+        return clientChannel != null && clientChannel.isOpen();
+    }
+
+    public int getClientPort() {
+        Objects.requireNonNull(clientChannel);
+        return (((InetSocketAddress) clientChannel.remoteAddress()).getPort());
+    }
+
+    public boolean isPeerVerified() {
+        try {
+            if (hasSecureConnection()) {
+                return sslHandler.engine().getSession().getPeerPrincipal() != null;
+            } else {
+                return false;
+            }
+        } catch (SSLPeerUnverifiedException unverified) {
+            return false;
+        }
+    }
+
+    public SSLEngine getConnectionSSLEngine() {
+        if (hasSecureConnection()) {
+            return sslHandler.engine();
+        } else {
+            return null;
+        }
+    }
+
+    public boolean isWebSocketServer() {
+        return options.isUseWebSockets();
+    }
+
+    public String getWebSocketPath() {
+        return webSocketPath;
+    }
+
+    public void setWebSocketPath(String webSocketPath) {
+        this.webSocketPath = webSocketPath;
+    }
+
+    public int getMaxFrameSize() {
+        return maxFrameSize;
+    }
+
+    public void setMaxFrameSize(int maxFrameSize) {
+        this.maxFrameSize = maxFrameSize;
+    }
+
+    public boolean awaitHandshakeCompletion(long delayMs) throws InterruptedException {
+        return handshakeCompletion.await(delayMs, TimeUnit.MILLISECONDS);
+    }
+
+    public HandshakeComplete getHandshakeComplete() {
+        return handshakeComplete;
+    }
+
+    public URI getConnectionURI() throws Exception {
+        if (!started.get()) {
+            throw new IllegalStateException("Cannot get URI of non-started server");
+        }
+
+        int port = getServerPort();
+
+        String scheme;
+        String path;
+
+        if (isWebSocketServer()) {
+            if (isSecureServer()) {
+                scheme = "amqpwss";
+            } else {
+                scheme = "amqpws";
+            }
+        } else {
+            if (isSecureServer()) {
+                scheme = "amqps";
+            } else {
+                scheme = "amqp";
+            }
+        }
+
+        if (isWebSocketServer()) {
+            path = getWebSocketPath();
+        } else {
+            path = null;
+        }
+
+        return new URI(scheme, null, "localhost", port, path, null, null);
+    }
+
+    public void start() throws Exception {
+        if (started.compareAndSet(false, true)) {
+            // Configure the server to basic NIO type channels
+            bossGroup = new NioEventLoopGroup(1);
+            workerGroup = new NioEventLoopGroup();
+
+            ServerBootstrap server = new ServerBootstrap();
+            server.group(bossGroup, workerGroup);
+            server.channel(NioServerSocketChannel.class);
+            server.option(ChannelOption.SO_BACKLOG, 100);
+            server.handler(new LoggingHandler(LogLevel.INFO));
+            server.childHandler(new ChannelInitializer<Channel>() {
+
+                @Override
+                public void initChannel(Channel ch) throws Exception {
+                    // Don't accept any new connections.
+                    serverChannel.close();
+                    // Now we know who the client is
+                    clientChannel = ch;
+
+                    if (isSecureServer()) {
+                        ch.pipeline().addLast(sslHandler = SslSupport.createServerSslHandler(null, options));
+                    }
+
+                    if (options.isUseWebSockets()) {
+                        ch.pipeline().addLast(new HttpServerCodec());
+                        ch.pipeline().addLast(new HttpObjectAggregator(65536));
+                        ch.pipeline().addLast(new WebSocketServerProtocolHandler(getWebSocketPath(), "amqp", true, maxFrameSize));
+                    }
+
+                    ch.pipeline().addLast(new NettyServerOutboundHandler());
+                    ch.pipeline().addLast(new NettyServerInboundHandler());
+                    ch.pipeline().addLast(getServerHandler());
+                }
+            });
+
+            // Start the server and then update the server port in case the configuration
+            // was such that the server chose a free port.
+            serverChannel = server.bind(options.getServerPort()).sync().channel();
+            options.setServerPort(((InetSocketAddress) serverChannel.localAddress()).getPort());
+        }
+    }
+
+    protected abstract ChannelHandler getServerHandler();
+
+    public void write(ByteBuffer frame) {
+        if (clientChannel == null || !clientChannel.isActive()) {
+            throw new IllegalStateException("Channel is not connected or has closed");
+        }
+
+        clientChannel.writeAndFlush(Unpooled.wrappedBuffer(frame), clientChannel.voidPromise());
+    }
+
+    public EventLoop eventLoop() {
+        if (clientChannel == null || !clientChannel.isActive()) {
+            throw new IllegalStateException("Channel is not connected or has closed");
+        }
+
+        return clientChannel.eventLoop();
+    }
+
+    public void stop() throws InterruptedException {
+        if (started.compareAndSet(true, false)) {
+            LOG.info("Syncing channel close");
+            serverChannel.close().syncUninterruptibly();
+
+            if (clientChannel != null) {
+                try {
+                    if (!clientChannel.close().await(10, TimeUnit.SECONDS)) {
+                        LOG.info("Connected Client channel close timed out wiating for result");
+                    }
+                } catch (InterruptedException e) {
+                    Thread.interrupted();
+                    LOG.debug("Close of connected client channel interrupted while awaiting result");
+                }
+            }
+
+            // Shut down all event loops to terminate all threads.
+            int timeout = 100;
+            LOG.trace("Shutting down boss group");
+            bossGroup.shutdownGracefully(0, timeout, TimeUnit.MILLISECONDS).awaitUninterruptibly(timeout);
+            LOG.trace("Boss group shut down");
+
+            LOG.trace("Shutting down worker group");
+            workerGroup.shutdownGracefully(0, timeout, TimeUnit.MILLISECONDS).awaitUninterruptibly(timeout);
+            LOG.trace("Worker group shut down");
+        }
+    }
+
+    @Override
+    public void close() throws InterruptedException {
+        stop();
+    }
+
+    public int getServerPort() {
+        if (!started.get()) {
+            throw new IllegalStateException("Cannot get server port of non-started server");
+        }
+
+        return options.getServerPort();
+    }
+
+    private class NettyServerOutboundHandler extends ChannelOutboundHandlerAdapter  {
+
+        @Override
+        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+            LOG.trace("NettyServerHandler: Channel write: {}", msg);
+            if (isWebSocketServer() && msg instanceof ByteBuf) {
+                if (options.isFragmentWrites()) {
+                    ByteBuf orig = (ByteBuf) msg;
+                    int origIndex = orig.readerIndex();
+                    int split = orig.readableBytes()/2;
+
+                    ByteBuf part1 = orig.copy(origIndex, split);
+                    LOG.trace("NettyServerHandler: Part1: {}", part1);
+                    orig.readerIndex(origIndex + split);
+                    LOG.trace("NettyServerHandler: Part2: {}", orig);
+
+                    BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false, 0, part1);
+                    ctx.writeAndFlush(frame1);
+                    ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(true, 0, orig);
+                    ctx.write(frame2, promise);
+                } else {
+                    BinaryWebSocketFrame frame = new BinaryWebSocketFrame((ByteBuf) msg);
+                    ctx.write(frame, promise);
+                }
+            } else {
+                ctx.write(msg, promise);
+            }
+        }
+    }
+
+    private class NettyServerInboundHandler extends ChannelInboundHandlerAdapter  {
+
+        @Override
+        public void userEventTriggered(ChannelHandlerContext context, Object payload) {
+            if (payload instanceof HandshakeComplete) {
+                handshakeComplete = (HandshakeComplete) payload;
+                handshakeCompletion.countDown();
+            }
+        }
+
+        @Override
+        public void channelActive(final ChannelHandlerContext ctx) {
+            LOG.info("NettyServerHandler -> New active channel: {}", ctx.channel());
+            SslHandler handler = ctx.pipeline().get(SslHandler.class);
+            if (handler != null) {
+                handler.handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {
+                    @Override
+                    public void operationComplete(Future<Channel> future) throws Exception {
+                        LOG.info("Server -> SSL handshake completed. Succeeded: {}", future.isSuccess());
+                        if (!future.isSuccess()) {
+                            ctx.close();
+                        }
+                    }
+                });
+            }
+
+            ctx.fireChannelActive();
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+            LOG.info("NettyServerHandler: channel has gone inactive: {}", ctx.channel());
+            ctx.close();
+            ctx.fireChannelInactive();
+        }
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object msg) {
+            LOG.trace("NettyServerHandler: Channel read: {}", msg);
+            if (msg instanceof WebSocketFrame) {
+                WebSocketFrame frame = (WebSocketFrame) msg;
+                ctx.fireChannelRead(frame.content());
+            } else if (msg instanceof FullHttpRequest) {
+                // Reject anything not on the WebSocket path
+                FullHttpRequest request = (FullHttpRequest) msg;
+                sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
+            } else {
+                // Forward anything else along to the next handler.
+                ctx.fireChannelRead(msg);
+            }
+        }
+
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx) {
+            ctx.flush();
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+            LOG.info("NettyServerHandler: NettyServerHandlerException caught on channel: {}", ctx.channel());
+            // Close the connection when an exception is raised.
+            cause.printStackTrace();
+            ctx.close();
+        }
+    }
+
+    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
+        // Generate an error page if response getStatus code is not OK (200).
+        if (response.status().code() != 200) {
+            ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), StandardCharsets.UTF_8);
+            response.content().writeBytes(buf);
+            buf.release();
+            HttpUtil.setContentLength(response, response.content().readableBytes());
+        }
+
+        // Send the response and close the connection if necessary.
+        ChannelFuture f = ctx.channel().writeAndFlush(response);
+        if (!HttpUtil.isKeepAlive(request) || response.status().code() != 200) {
+            f.addListener(ChannelFutureListener.CLOSE);
+        }
+    }
+
+    protected SslHandler getSslHandler() {
+        return sslHandler;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/SslSupport.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/SslSupport.java
new file mode 100644
index 0000000..e0b3c49
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/SslSupport.java
@@ -0,0 +1,502 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.netty;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+import org.apache.qpid.protonj2.test.driver.ProtonTestClientOptions;
+import org.apache.qpid.protonj2.test.driver.ProtonTestServerOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+
+/**
+ * Static class that provides various utility methods used by Transport implementations.
+ */
+public class SslSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SslSupport.class);
+
+    /**
+     * Creates a Netty SslHandler instance for use in client instances that require
+     * an SSL encoder / decoder.
+     *
+     * If the given options contain an SSLContext override, this will be used directly
+     * when creating the handler. If they do not, an SSLContext will first be created
+     * using the other option values.
+     *
+     * @param allocator
+     *		  The Netty Buffer Allocator to use when Netty resources need to be created.
+     * @param remote
+     *        The URI of the remote peer that the SslHandler will be used against.
+     * @param options
+     *        The SSL options object to build the SslHandler instance from.
+     *
+     * @return a new SslHandler that is configured from the given options.
+     *
+     * @throws Exception if an error occurs while creating the SslHandler instance.
+     */
+    public static SslHandler createClientSslHandler(URI remote, ProtonTestClientOptions options) throws Exception {
+        final SSLEngine sslEngine;
+
+        SSLContext sslContext = options.getSslContextOverride();
+        if (sslContext == null) {
+            sslContext = createClientJdkSslContext(options);
+        }
+
+        sslEngine = createClientJdkSslEngine(remote, sslContext, options);
+
+        return new SslHandler(sslEngine);
+    }
+
+    /**
+     * Creates a Netty SslHandler instance for use in server instances that require
+     * an SSL encoder / decoder.
+     *
+     * If the given options contain an SSLContext override, this will be used directly
+     * when creating the handler. If they do not, an SSLContext will first be created
+     * using the other option values.
+     *
+     * @param allocator
+     *        The Netty Buffer Allocator to use when Netty resources need to be created.
+     * @param remote
+     *        The URI of the remote peer that the SslHandler will be used against.
+     * @param options
+     *        The SSL options object to build the SslHandler instance from.
+     *
+     * @return a new SslHandler that is configured from the given options.
+     *
+     * @throws Exception if an error occurs while creating the SslHandler instance.
+     */
+    public static SslHandler createServerSslHandler(URI remote, ProtonTestServerOptions options) throws Exception {
+        final SSLEngine sslEngine;
+
+        SSLContext sslContext = options.getSslContextOverride();
+        if (sslContext == null) {
+            sslContext = createServerJdkSslContext(options);
+        }
+
+        sslEngine = createServerJdkSslEngine(remote, sslContext, options);
+
+        return new SslHandler(sslEngine);
+    }
+
+    //----- JDK SSL Support Methods ------------------------------------------//
+
+    /**
+     * Create a new SSLContext using the options specific in the given TransportOptions
+     * instance.
+     *
+     * @param options
+     *        the configured options used to create the SSLContext.
+     *
+     * @return a new SSLContext instance.
+     *
+     * @throws Exception if an error occurs while creating the context.
+     */
+    public static SSLContext createClientJdkSslContext(ProtonTestClientOptions options) throws Exception {
+        try {
+            String contextProtocol = options.getContextProtocol();
+            LOG.trace("Getting SSLContext instance using protocol: {}", contextProtocol);
+
+            SSLContext context = SSLContext.getInstance(contextProtocol);
+
+            KeyManager[] keyMgrs = loadKeyManagers(options);
+            TrustManager[] trustManagers = loadTrustManagers(options);
+
+            context.init(keyMgrs, trustManagers, new SecureRandom());
+            return context;
+        } catch (Exception e) {
+            LOG.error("Failed to create SSLContext: {}", e, e);
+            throw e;
+        }
+    }
+
+    /**
+     * Create a new SSLContext using the options specific in the given TransportOptions
+     * instance.
+     *
+     * @param options
+     *        the configured options used to create the SSLContext.
+     *
+     * @return a new SSLContext instance.
+     *
+     * @throws Exception if an error occurs while creating the context.
+     */
+    public static SSLContext createServerJdkSslContext(ProtonTestServerOptions options) throws Exception {
+        try {
+            String contextProtocol = options.getContextProtocol();
+            LOG.trace("Getting SSLContext instance using protocol: {}", contextProtocol);
+
+            SSLContext context = SSLContext.getInstance(contextProtocol);
+
+            KeyManager[] keyMgrs = loadKeyManagers(options);
+            TrustManager[] trustManagers = loadTrustManagers(options);
+
+            context.init(keyMgrs, trustManagers, new SecureRandom());
+            return context;
+        } catch (Exception e) {
+            LOG.error("Failed to create SSLContext: {}", e, e);
+            throw e;
+        }
+    }
+
+    /**
+     * Create a new JDK SSLEngine instance in client mode from the given SSLContext and
+     * TransportOptions instances.
+     *
+     * @param remote
+     *        the URI of the remote peer that will be used to initialize the engine, may be null if none should.
+     * @param context
+     *        the SSLContext to use when creating the engine.
+     * @param client
+     *        indicates if the context is meant for use with a client or sever
+     * @param options
+     *        the TransportOptions to use to configure the new SSLEngine.
+     *
+     * @return a new SSLEngine instance in client mode.
+     *
+     * @throws Exception if an error occurs while creating the new SSLEngine.
+     */
+    public static SSLEngine createClientJdkSslEngine(URI remote, SSLContext context, ProtonTestClientOptions options) throws Exception {
+        SSLEngine engine = null;
+        if (remote == null) {
+            engine = context.createSSLEngine();
+        } else {
+            engine = context.createSSLEngine(remote.getHost(), remote.getPort());
+        }
+
+        engine.setEnabledProtocols(buildEnabledProtocols(engine, options));
+        engine.setEnabledCipherSuites(buildEnabledCipherSuites(engine, options));
+        engine.setUseClientMode(true);
+        engine.setNeedClientAuth(options.isNeedClientAuth());
+
+        if (options.isVerifyHost()) {
+            SSLParameters sslParameters = engine.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            engine.setSSLParameters(sslParameters);
+        }
+
+        return engine;
+    }
+
+    /**
+     * Create a new JDK SSLEngine instance in client mode from the given SSLContext and
+     * TransportOptions instances.
+     *
+     * @param remote
+     *        the URI of the remote peer that will be used to initialize the engine, may be null if none should.
+     * @param context
+     *        the SSLContext to use when creating the engine.
+     * @param options
+     *        the TransportOptions to use to configure the new SSLEngine.
+     *
+     * @return a new SSLEngine instance in client mode.
+     *
+     * @throws Exception if an error occurs while creating the new SSLEngine.
+     */
+    public static SSLEngine createServerJdkSslEngine(URI remote, SSLContext context, ProtonTestServerOptions options) throws Exception {
+        SSLEngine engine = null;
+        if (remote == null) {
+            engine = context.createSSLEngine();
+        } else {
+            engine = context.createSSLEngine(remote.getHost(), remote.getPort());
+        }
+
+        engine.setEnabledProtocols(buildEnabledProtocols(engine, options));
+        engine.setEnabledCipherSuites(buildEnabledCipherSuites(engine, options));
+        engine.setUseClientMode(false);
+        engine.setNeedClientAuth(options.isNeedClientAuth());
+
+        if (options.isVerifyHost()) {
+            SSLParameters sslParameters = engine.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            engine.setSSLParameters(sslParameters);
+        }
+
+        return engine;
+    }
+
+    //----- Internal support methods -----------------------------------------//
+
+    private static String[] buildEnabledProtocols(SSLEngine engine, ProtonTestClientOptions options) {
+        List<String> enabledProtocols = new ArrayList<>();
+
+        if (options.getEnabledProtocols() != null) {
+            List<String> configuredProtocols = Arrays.asList(options.getEnabledProtocols());
+            LOG.trace("Configured protocols from transport options: {}", configuredProtocols);
+            enabledProtocols.addAll(configuredProtocols);
+        } else {
+            List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+            LOG.trace("Default protocols from the SSLEngine: {}", engineProtocols);
+            enabledProtocols.addAll(engineProtocols);
+        }
+
+        String[] disabledProtocols = options.getDisabledProtocols();
+        if (disabledProtocols != null) {
+            List<String> disabled = Arrays.asList(disabledProtocols);
+            LOG.trace("Disabled protocols: {}", disabled);
+            enabledProtocols.removeAll(disabled);
+        }
+
+        LOG.trace("Enabled protocols: {}", enabledProtocols);
+
+        return enabledProtocols.toArray(new String[0]);
+    }
+
+    private static String[] buildEnabledProtocols(SSLEngine engine, ProtonTestServerOptions options) {
+        List<String> enabledProtocols = new ArrayList<>();
+
+        if (options.getEnabledProtocols() != null) {
+            List<String> configuredProtocols = Arrays.asList(options.getEnabledProtocols());
+            LOG.trace("Configured protocols from transport options: {}", configuredProtocols);
+            enabledProtocols.addAll(configuredProtocols);
+        } else {
+            List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+            LOG.trace("Default protocols from the SSLEngine: {}", engineProtocols);
+            enabledProtocols.addAll(engineProtocols);
+        }
+
+        String[] disabledProtocols = options.getDisabledProtocols();
+        if (disabledProtocols != null) {
+            List<String> disabled = Arrays.asList(disabledProtocols);
+            LOG.trace("Disabled protocols: {}", disabled);
+            enabledProtocols.removeAll(disabled);
+        }
+
+        LOG.trace("Enabled protocols: {}", enabledProtocols);
+
+        return enabledProtocols.toArray(new String[0]);
+    }
+
+    private static String[] buildEnabledCipherSuites(SSLEngine engine, ProtonTestServerOptions options) {
+        List<String> enabledCipherSuites = new ArrayList<>();
+
+        if (options.getEnabledCipherSuites() != null) {
+            List<String> configuredCipherSuites = Arrays.asList(options.getEnabledCipherSuites());
+            LOG.trace("Configured cipher suites from transport options: {}", configuredCipherSuites);
+            enabledCipherSuites.addAll(configuredCipherSuites);
+        } else {
+            List<String> engineCipherSuites = Arrays.asList(engine.getEnabledCipherSuites());
+            LOG.trace("Default cipher suites from the SSLEngine: {}", engineCipherSuites);
+            enabledCipherSuites.addAll(engineCipherSuites);
+        }
+
+        String[] disabledCipherSuites = options.getDisabledCipherSuites();
+        if (disabledCipherSuites != null) {
+            List<String> disabled = Arrays.asList(disabledCipherSuites);
+            LOG.trace("Disabled cipher suites: {}", disabled);
+            enabledCipherSuites.removeAll(disabled);
+        }
+
+        LOG.trace("Enabled cipher suites: {}", enabledCipherSuites);
+
+        return enabledCipherSuites.toArray(new String[0]);
+    }
+
+    private static String[] buildEnabledCipherSuites(SSLEngine engine, ProtonTestClientOptions options) {
+        List<String> enabledCipherSuites = new ArrayList<>();
+
+        if (options.getEnabledCipherSuites() != null) {
+            List<String> configuredCipherSuites = Arrays.asList(options.getEnabledCipherSuites());
+            LOG.trace("Configured cipher suites from transport options: {}", configuredCipherSuites);
+            enabledCipherSuites.addAll(configuredCipherSuites);
+        } else {
+            List<String> engineCipherSuites = Arrays.asList(engine.getEnabledCipherSuites());
+            LOG.trace("Default cipher suites from the SSLEngine: {}", engineCipherSuites);
+            enabledCipherSuites.addAll(engineCipherSuites);
+        }
+
+        String[] disabledCipherSuites = options.getDisabledCipherSuites();
+        if (disabledCipherSuites != null) {
+            List<String> disabled = Arrays.asList(disabledCipherSuites);
+            LOG.trace("Disabled cipher suites: {}", disabled);
+            enabledCipherSuites.removeAll(disabled);
+        }
+
+        LOG.trace("Enabled cipher suites: {}", enabledCipherSuites);
+
+        return enabledCipherSuites.toArray(new String[0]);
+    }
+
+    private static TrustManager[] loadTrustManagers(ProtonTestClientOptions options) throws Exception {
+        TrustManagerFactory factory = loadTrustManagerFactory(options);
+        if (factory != null) {
+            return factory.getTrustManagers();
+        } else {
+            return null;
+        }
+    }
+
+    private static TrustManager[] loadTrustManagers(ProtonTestServerOptions options) throws Exception {
+        TrustManagerFactory factory = loadTrustManagerFactory(options);
+        if (factory != null) {
+            return factory.getTrustManagers();
+        } else {
+            return null;
+        }
+    }
+
+    private static TrustManagerFactory loadTrustManagerFactory(ProtonTestClientOptions options) throws Exception {
+        if (options.isTrustAll()) {
+            return InsecureTrustManagerFactory.INSTANCE;
+        }
+
+        if (options.getTrustStoreLocation() == null) {
+            return null;
+        }
+
+        TrustManagerFactory fact = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+        String storeLocation = options.getTrustStoreLocation();
+        String storePassword = options.getTrustStorePassword();
+        String storeType = options.getTrustStoreType();
+
+        LOG.trace("Attempt to load TrustStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore trustStore = loadStore(storeLocation, storePassword, storeType);
+        fact.init(trustStore);
+
+        return fact;
+    }
+
+    private static TrustManagerFactory loadTrustManagerFactory(ProtonTestServerOptions options) throws Exception {
+        if (options.isTrustAll()) {
+            return InsecureTrustManagerFactory.INSTANCE;
+        }
+
+        if (options.getTrustStoreLocation() == null) {
+            return null;
+        }
+
+        TrustManagerFactory fact = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+        String storeLocation = options.getTrustStoreLocation();
+        String storePassword = options.getTrustStorePassword();
+        String storeType = options.getTrustStoreType();
+
+        LOG.trace("Attempt to load TrustStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore trustStore = loadStore(storeLocation, storePassword, storeType);
+        fact.init(trustStore);
+
+        return fact;
+    }
+
+    private static KeyManager[] loadKeyManagers(ProtonTestClientOptions options) throws Exception {
+        if (options.getKeyStoreLocation() == null) {
+            return null;
+        }
+
+        KeyManagerFactory fact = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+        String storeLocation = options.getKeyStoreLocation();
+        String storePassword = options.getKeyStorePassword();
+        String storeType = options.getKeyStoreType();
+        String alias = options.getKeyAlias();
+
+        LOG.trace("Attempt to load KeyStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore keyStore = loadStore(storeLocation, storePassword, storeType);
+        fact.init(keyStore, storePassword != null ? storePassword.toCharArray() : null);
+
+        if (alias == null) {
+            return fact.getKeyManagers();
+        } else {
+            validateAlias(keyStore, alias);
+            return wrapKeyManagers(alias, fact.getKeyManagers());
+        }
+    }
+
+    private static KeyManager[] loadKeyManagers(ProtonTestServerOptions options) throws Exception {
+        if (options.getKeyStoreLocation() == null) {
+            return null;
+        }
+
+        KeyManagerFactory fact = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+        String storeLocation = options.getKeyStoreLocation();
+        String storePassword = options.getKeyStorePassword();
+        String storeType = options.getKeyStoreType();
+        String alias = options.getKeyAlias();
+
+        LOG.trace("Attempt to load KeyStore from location {} of type {}", storeLocation, storeType);
+
+        KeyStore keyStore = loadStore(storeLocation, storePassword, storeType);
+        fact.init(keyStore, storePassword != null ? storePassword.toCharArray() : null);
+
+        if (alias == null) {
+            return fact.getKeyManagers();
+        } else {
+            validateAlias(keyStore, alias);
+            return wrapKeyManagers(alias, fact.getKeyManagers());
+        }
+    }
+
+    private static KeyManager[] wrapKeyManagers(String alias, KeyManager[] origKeyManagers) {
+        KeyManager[] keyManagers = new KeyManager[origKeyManagers.length];
+        for (int i = 0; i < origKeyManagers.length; i++) {
+            KeyManager km = origKeyManagers[i];
+            if (km instanceof X509ExtendedKeyManager) {
+                km = new X509AliasKeyManager(alias, (X509ExtendedKeyManager) km);
+            }
+
+            keyManagers[i] = km;
+        }
+
+        return keyManagers;
+    }
+
+    private static void validateAlias(KeyStore store, String alias) throws IllegalArgumentException, KeyStoreException {
+        if (!store.containsAlias(alias)) {
+            throw new IllegalArgumentException("The alias '" + alias + "' doesn't exist in the key store");
+        }
+
+        if (!store.isKeyEntry(alias)) {
+            throw new IllegalArgumentException("The alias '" + alias + "' in the keystore doesn't represent a key entry");
+        }
+    }
+
+    private static KeyStore loadStore(String storePath, final String password, String storeType) throws Exception {
+        KeyStore store = KeyStore.getInstance(storeType);
+        try (InputStream in = new FileInputStream(new File(storePath));) {
+            store.load(in, password != null ? password.toCharArray() : null);
+        }
+
+        return store;
+    }
+}
diff --git a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/X509AliasKeyManager.java b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/X509AliasKeyManager.java
new file mode 100644
index 0000000..acb2849
--- /dev/null
+++ b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/netty/X509AliasKeyManager.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.netty;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+/**
+ * An X509ExtendedKeyManager wrapper which always chooses and only
+ * returns the given alias, and defers retrieval to the delegate
+ * key manager.
+ */
+public class X509AliasKeyManager extends X509ExtendedKeyManager {
+
+    private X509ExtendedKeyManager delegate;
+    private String alias;
+
+    public X509AliasKeyManager(String alias, X509ExtendedKeyManager delegate) throws IllegalArgumentException {
+        if (alias == null) {
+            throw new IllegalArgumentException("The given key alias must not be null.");
+        }
+
+        this.alias = alias;
+        this.delegate = delegate;
+    }
+
+    @Override
+    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+        return alias;
+    }
+
+    @Override
+    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+        return alias;
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain(String alias) {
+        return delegate.getCertificateChain(alias);
+    }
+
+    @Override
+    public String[] getClientAliases(String keyType, Principal[] issuers) {
+        return new String[] { alias };
+    }
+
+    @Override
+    public PrivateKey getPrivateKey(String alias) {
+        return delegate.getPrivateKey(alias);
+    }
+
+    @Override
+    public String[] getServerAliases(String keyType, Principal[] issuers) {
+        return new String[] { alias };
+    }
+
+    @Override
+    public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+        return alias;
+    }
+
+    @Override
+    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+        return alias;
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/codec/benchmark/Benchmark.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/codec/benchmark/Benchmark.java
new file mode 100644
index 0000000..3911447
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/codec/benchmark/Benchmark.java
@@ -0,0 +1,447 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.codec.benchmark;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.test.driver.codec.Codec;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Accepted;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Data;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Header;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Properties;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Binary;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.Symbol;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedByte;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Disposition;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Flow;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Transfer;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+public class Benchmark implements Runnable {
+
+    private static final int ITERATIONS = 10 * 1024 * 1024;
+
+    ByteBuf buffer = Unpooled.buffer(8192);
+    private BenchmarkResult resultSet = new BenchmarkResult();
+    private boolean warming = true;
+
+    private Codec codec = Codec.Factory.create();
+
+    public static final void main(String[] args) throws IOException, InterruptedException {
+        System.out.println("Current PID: " + ManagementFactory.getRuntimeMXBean().getName());
+        Benchmark benchmark = new Benchmark();
+        benchmark.run();
+    }
+
+    @Override
+    public void run() {
+        try {
+            doBenchmarks();
+            warming = false;
+            doBenchmarks();
+        } catch (IOException e) {
+            System.out.println("Unexpected error: " + e.getMessage());
+        }
+    }
+
+    private void time(String message, BenchmarkResult resultSet) {
+        if (!warming) {
+            System.out.println("Benchamrk of type: " + message + ": ");
+            System.out.println("    Encode time = " + resultSet.getEncodeTimeMills());
+            System.out.println("    Decode time = " + resultSet.getDecodeTimeMills());
+        }
+    }
+
+    private final void doBenchmarks() throws IOException {
+        benchmarkListOfInts();
+        benchmarkUUIDs();
+        benchmarkHeader();
+        benchmarkProperties();
+        benchmarkMessageAnnotations();
+        benchmarkApplicationProperties();
+        benchmarkSymbols();
+        benchmarkTransfer();
+        benchmarkFlow();
+        benchmarkDisposition();
+        benchmarkString();
+        benchmarkData();
+    }
+
+    private void benchmarkListOfInts() throws IOException {
+        ArrayList<Object> list = new ArrayList<>(10);
+        for (int j = 0; j < 10; j++) {
+            list.add(0);
+        }
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putJavaList(list);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("List<Integer>", resultSet);
+    }
+
+    private void benchmarkUUIDs() throws IOException {
+        UUID uuid = UUID.randomUUID();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putUUID(uuid);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("UUID", resultSet);
+    }
+
+    private void benchmarkTransfer() throws IOException {
+        Transfer transfer = new Transfer();
+        transfer.setDeliveryTag(new Binary(new byte[] {1, 2, 3}));
+        transfer.setHandle(UnsignedInteger.valueOf(1024));
+        transfer.setMessageFormat(UnsignedInteger.valueOf(0));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(transfer);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Transfer", resultSet);
+    }
+
+    private void benchmarkFlow() throws IOException {
+        Flow flow = new Flow();
+        flow.setNextIncomingId(UnsignedInteger.valueOf(1));
+        flow.setIncomingWindow(UnsignedInteger.valueOf(2047));
+        flow.setNextOutgoingId(UnsignedInteger.valueOf(1));
+        flow.setOutgoingWindow(UnsignedInteger.MAX_VALUE);
+        flow.setHandle(UnsignedInteger.ZERO);
+        flow.setDeliveryCount(UnsignedInteger.valueOf(10));
+        flow.setLinkCredit(UnsignedInteger.valueOf(1000));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(flow);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Flow", resultSet);
+    }
+
+    private void benchmarkHeader() throws IOException {
+        Header header = new Header();
+        header.setDurable(true);
+        header.setFirstAcquirer(true);
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(header);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Header", resultSet);
+    }
+
+    private void benchmarkProperties() throws IOException {
+        Properties properties = new Properties();
+        properties.setTo("queue:1-1024");
+        properties.setReplyTo("queue:1-11024-reply");
+        properties.setMessageId("ID:255f1297-5a71-4df1-8147-b2cdf850a56f:1");
+        properties.setCreationTime(new Date(System.currentTimeMillis()));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(properties);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Properties", resultSet);
+    }
+
+    private void benchmarkMessageAnnotations() throws IOException {
+        MessageAnnotations annotations = new MessageAnnotations();
+        annotations.getDescribed().put(Symbol.valueOf("test1"), UnsignedByte.valueOf((byte) 128));
+        annotations.getDescribed().put(Symbol.valueOf("test2"), UnsignedShort.valueOf((short) 128));
+        annotations.getDescribed().put(Symbol.valueOf("test3"), UnsignedInteger.valueOf((byte) 128));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(annotations);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("MessageAnnotations", resultSet);
+    }
+
+    private void benchmarkApplicationProperties() throws IOException {
+        ApplicationProperties properties = new ApplicationProperties();
+        properties.getDescribed().put("test1", UnsignedByte.valueOf((byte) 128));
+        properties.getDescribed().put("test2", UnsignedShort.valueOf((short) 128));
+        properties.getDescribed().put("test3", UnsignedInteger.valueOf((byte) 128));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(properties);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("ApplicationProperties", resultSet);
+    }
+
+    private void benchmarkSymbols() throws IOException {
+        Symbol symbol1 = Symbol.valueOf("Symbol-1");
+        Symbol symbol2 = Symbol.valueOf("Symbol-2");
+        Symbol symbol3 = Symbol.valueOf("Symbol-3");
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putSymbol(symbol1);
+            codec.putSymbol(symbol2);
+            codec.putSymbol(symbol3);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Symbol", resultSet);
+    }
+
+    private void benchmarkString() throws IOException {
+        String string1 = new String("String-1-somewhat-long-test-to-validate-performance-improvements-to-the-proton-j-codec-@!%$");
+        String string2 = new String("String-2-somewhat-long-test-to-validate-performance-improvements-to-the-proton-j-codec-@!%$");
+        String string3 = new String("String-3-somewhat-long-test-to-validate-performance-improvements-to-the-proton-j-codec-@!%$");
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putString(string1);
+            codec.putString(string2);
+            codec.putString(string3);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.decode(buffer);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("String", resultSet);
+    }
+
+    private void benchmarkDisposition() throws IOException {
+        Disposition disposition = new Disposition();
+        disposition.setRole(Role.RECEIVER.getValue());
+        disposition.setSettled(true);
+        disposition.setState(new Accepted());
+        disposition.setFirst(UnsignedInteger.valueOf(2));
+        disposition.setLast(UnsignedInteger.valueOf(2));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(disposition);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Disposition", resultSet);
+    }
+
+    private void benchmarkData() throws IOException {
+        Data data1 = new Data(new Binary(new byte[] {1, 2, 3}));
+        Data data2 = new Data(new Binary(new byte[] {4, 5, 6}));
+        Data data3 = new Data(new Binary(new byte[] {7, 8, 9}));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            codec.putDescribedType(data1);
+            codec.putDescribedType(data2);
+            codec.putDescribedType(data3);
+            codec.encode(buffer);
+            codec.clear();
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.readerIndex(0);
+            codec.decode(buffer);
+            codec.clear();
+        }
+        resultSet.decodesComplete();
+
+        time("Data", resultSet);
+    }
+
+    private static class BenchmarkResult {
+
+        private long startTime;
+
+        private long encodeTime;
+        private long decodeTime;
+
+        public void start() {
+            startTime = System.nanoTime();
+        }
+
+        public void encodesComplete() {
+            encodeTime = System.nanoTime() - startTime;
+        }
+
+        public void decodesComplete() {
+            decodeTime = System.nanoTime() - startTime;
+        }
+
+        public long getEncodeTimeMills() {
+            return TimeUnit.NANOSECONDS.toMillis(encodeTime);
+        }
+
+        public long getDecodeTimeMills() {
+            return TimeUnit.NANOSECONDS.toMillis(decodeTime);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/LinkHandlingTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/LinkHandlingTest.java
new file mode 100644
index 0000000..39c04b6
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/LinkHandlingTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.utils.TestPeerTestsBase;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class LinkHandlingTest extends TestPeerTestsBase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LinkHandlingTest.class);
+
+    @Test
+    public void testClientToServerSenderAttach() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+            ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0).respond();
+            peer.expectAttach().ofSender().onChannel(0).respond();
+            peer.expectDetach().onChannel(0).respond();
+            peer.expectEnd().onChannel(0).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectBegin().onChannel(0);
+            client.expectAttach().ofReceiver().withHandle(0);
+            client.expectDetach().withHandle(0);
+            client.expectEnd();
+            client.expectClose();
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            // This initiates the tests and waits for proper completion.
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+            client.remoteAttach().ofSender().now();
+            client.remoteDetach().now();
+            client.remoteEnd().now();
+            client.remoteClose().now();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestClientTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestClientTest.java
new file mode 100644
index 0000000..6c56ce6
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestClientTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.utils.TestPeerTestsBase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests the basics of the Proton Test Client implementation
+ */
+@Timeout(20)
+class ProtonTestClientTest extends TestPeerTestsBase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ProtonTestClientTest.class);
+
+    @Test
+    public void testClientCanConnectAndExchangeAMQPHeaders() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ProtonTestClient client = new ProtonTestClient();
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testClientDetectsUnexpectedPerformativeResponseToAMQPHeader() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectAMQPHeader();
+            peer.remoteOpen().queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ProtonTestClient client = new ProtonTestClient();
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+
+            assertThrows(AssertionError.class, () -> client.waitForScriptToComplete(5, TimeUnit.SECONDS));
+
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testClientDetactsUnexpectedPerformativeAndFailsTest() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen();
+            peer.remoteBegin().queue();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ProtonTestClient client = new ProtonTestClient();
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+
+            assertThrows(AssertionError.class, () -> client.waitForScriptToComplete(5, TimeUnit.SECONDS));
+
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testClientCanConnectAndOpenExchanged() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ProtonTestClient client = new ProtonTestClient();
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectClose();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteClose().now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java
new file mode 100644
index 0000000..c0fe569
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.test.driver.utils.TestPeerTestsBase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Timeout(20)
+public class ProtonTestServerTest extends TestPeerTestsBase {
+
+    @Test
+    public void testServerStart() throws Exception {
+        ProtonTestServer peer = new ProtonTestServer();
+
+        assertFalse(peer.isClosed());
+        peer.start();
+        peer.close();
+        assertTrue(peer.isClosed());
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestWSClientTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestWSClientTest.java
new file mode 100644
index 0000000..458b476
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestWSClientTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.utils.TestPeerTestsBase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests the basics of the Proton Test Client implementation
+ */
+@Timeout(20)
+class ProtonTestWSClientTest extends TestPeerTestsBase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ProtonTestWSClientTest.class);
+
+    @Test
+    public void testClientCanConnectAndExchangeAMQPHeaders() throws Exception {
+        ProtonTestServerOptions serverOpts = new ProtonTestServerOptions();
+        serverOpts.setUseWebSockets(true);
+
+        ProtonTestClientOptions clientOpts = new ProtonTestClientOptions();
+        clientOpts.setUseWebSockets(true);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOpts)) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ProtonTestClient client = new ProtonTestClient(clientOpts);
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testClientCanConnectAndOpenExchanged() throws Exception {
+        ProtonTestServerOptions serverOpts = new ProtonTestServerOptions();
+        serverOpts.setUseWebSockets(true);
+
+        ProtonTestClientOptions clientOpts = new ProtonTestClientOptions();
+        clientOpts.setUseWebSockets(true);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOpts)) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ProtonTestClient client = new ProtonTestClient(clientOpts);
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectClose();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteClose().now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/SessionHandlingTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/SessionHandlingTest.java
new file mode 100644
index 0000000..01b5948
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/SessionHandlingTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
+import org.apache.qpid.protonj2.test.driver.utils.TestPeerTestsBase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests for the test driver session handling from both client and server perspectives.
+ */
+@Timeout(20)
+class SessionHandlingTest extends TestPeerTestsBase {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SessionHandlingTest.class);
+
+    @Test
+    public void testSessionTrackingWithClientOpensSession() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+             ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0).respond();
+            peer.expectEnd().onChannel(0).respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectBegin().onChannel(0);
+            client.expectEnd().onChannel(0);
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+            client.remoteEnd().now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.close();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSessionBeginResponseUsesScriptedChannel() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+             ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0).respond().onChannel(42);
+            peer.expectEnd().onChannel(0).respond().onChannel(42);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectBegin().withRemoteChannel(0).onChannel(42);
+            client.expectEnd().onChannel(42);
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+            client.remoteEnd().now();
+
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testWaitForCompletionFailsWhenRemoteSendEndOnWrongChannel() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+             ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0).respond().onChannel(42);
+            peer.expectEnd().onChannel(0).respond().onChannel(43);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectBegin().withRemoteChannel(0).onChannel(42);
+            client.expectEnd().onChannel(42);
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+            client.remoteEnd().now();
+
+            assertThrows(AssertionError.class, () -> client.waitForScriptToComplete(5, TimeUnit.SECONDS));
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testServerEndResponseFillsChannelsAutomaticallyIfNoneSpecified() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+             ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0).respond().onChannel(42);
+            peer.expectEnd().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectBegin().withRemoteChannel(0).onChannel(42);
+            client.expectEnd().onChannel(42);
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+            client.remoteEnd().now();
+
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testServerRespondToLastBeginFeature() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+             ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+
+            // Wait for the above and then script next steps
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.expectBegin().withRemoteChannel(0).onChannel(42);
+
+            // Now we respond to the last begin we saw at the server side.
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.expectEnd().respond();
+            peer.respondToLastBegin().onChannel(42).now();
+
+            // Wait for the above and then script next steps
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.expectEnd().onChannel(42);
+            client.remoteEnd().now();
+
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testOpenAndCloseMultipleSessionsWithAutoChannelHandlingExpected() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer();
+             ProtonTestClient client = new ProtonTestClient()) {
+
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            peer.expectBegin().onChannel(0).respond();
+            peer.expectBegin().onChannel(1).respond();
+            peer.expectBegin().onChannel(2).respond();
+            peer.expectEnd().onChannel(2).respond();
+            peer.expectEnd().onChannel(1).respond();
+            peer.expectEnd().onChannel(0).respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            LOG.info("Test started, peer listening on: {}", remoteURI);
+
+            client.expectAMQPHeader();
+            client.expectOpen();
+            client.expectBegin().onChannel(0);
+            client.expectBegin().onChannel(1);
+            client.expectBegin().onChannel(2);
+            client.connect(remoteURI.getHost(), remoteURI.getPort());
+
+            client.remoteHeader(AMQPHeader.getAMQPHeader()).now();
+            client.remoteOpen().now();
+            client.remoteBegin().now();
+            client.remoteBegin().now();
+            client.remoteBegin().now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.expectEnd().onChannel(2);
+            client.expectEnd().onChannel(1);
+            client.expectEnd().onChannel(0);
+
+            client.remoteEnd().onChannel(2).now();
+            client.remoteEnd().onChannel(1).now();
+            client.remoteEnd().onChannel(0).now();
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            client.expectClose();
+
+            client.remoteClose().now();
+
+            client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/NettyReadableBufferTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/NettyReadableBufferTest.java
new file mode 100644
index 0000000..c54e3c4
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/NettyReadableBufferTest.java
@@ -0,0 +1,458 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Tests for the {@link ReadableBuffer} wrapper that uses Netty ByteBuf underneath
+ */
+public class NettyReadableBufferTest {
+
+    @Test
+    public void testWrapBuffer() {
+        ByteBuf byteBuffer = Unpooled.buffer(100, 100);
+
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(100, buffer.capacity());
+        assertSame(byteBuffer, buffer.getBuffer());
+        assertSame(buffer, buffer.reclaimRead());
+    }
+
+    @Test
+    public void testArrayAccess() {
+        ByteBuf byteBuffer = Unpooled.buffer(100, 100);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertTrue(buffer.hasArray());
+        assertSame(buffer.array(), byteBuffer.array());
+        assertEquals(buffer.arrayOffset(), byteBuffer.arrayOffset());
+    }
+
+    @Test
+    public void testArrayOffset() {
+        byte[] data = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data, 5, 5);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertTrue(buffer.hasArray());
+        assertSame(buffer.array(), byteBuffer.array());
+        assertEquals(buffer.arrayOffset(), byteBuffer.arrayOffset());
+
+        assertEquals(5, buffer.get());
+
+        assertEquals(buffer.arrayOffset(), byteBuffer.arrayOffset());
+    }
+
+    @Test
+    public void testArrayAccessWhenNoArray() {
+        ByteBuf byteBuffer = Unpooled.directBuffer();
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertFalse(buffer.hasArray());
+    }
+
+    @Test
+    public void testByteBuffer() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        ByteBuffer nioBuffer = buffer.byteBuffer();
+        assertEquals(data.length, nioBuffer.remaining());
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], nioBuffer.get());
+        }
+    }
+
+    @Test
+    public void testGet() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.get();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetIndex() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get(i));
+        }
+
+        assertTrue(buffer.hasRemaining());
+    }
+
+    @Test
+    public void testGetShort() {
+        byte[] data = new byte[] { 0, 1 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(1, buffer.getShort());
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.getShort();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetInt() {
+        byte[] data = new byte[] { 0, 0, 0, 1 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(1, buffer.getInt());
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.getInt();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetLong() {
+        byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(1, buffer.getLong());
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.getLong();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetFloat() {
+        byte[] data = new byte[] { 0, 0, 0, 0 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(0, buffer.getFloat(), 0.0);
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.getFloat();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetDouble() {
+        byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(0, buffer.getDouble(), 0.0);
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.getDouble();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetBytes() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        byte[] target = new byte[data.length];
+
+        buffer.get(target);
+        assertFalse(buffer.hasRemaining());
+        assertArrayEquals(data, target);
+
+        try {
+            buffer.get(target);
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetBytesIntInt() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        byte[] target = new byte[data.length];
+
+        buffer.get(target, 0, target.length);
+        assertFalse(buffer.hasRemaining());
+        assertArrayEquals(data, target);
+
+        try {
+            buffer.get(target, 0, target.length);
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testGetBytesToWritableBuffer() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+        ByteBuf targetBuffer = Unpooled.buffer(data.length, data.length);
+        NettyWritableBuffer target = new NettyWritableBuffer(targetBuffer);
+
+        buffer.get(target);
+        assertFalse(buffer.hasRemaining());
+        assertArrayEquals(targetBuffer.array(), data);
+    }
+
+    @Test
+    public void testGetBytesToWritableBufferThatIsDirect() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.directBuffer(data.length, data.length);
+        byteBuffer.writeBytes(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+        ByteBuf targetBuffer = Unpooled.buffer(data.length, data.length);
+        NettyWritableBuffer target = new NettyWritableBuffer(targetBuffer);
+
+        buffer.get(target);
+        assertFalse(buffer.hasRemaining());
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], target.getBuffer().readByte());
+        }
+    }
+
+    @Test
+    public void testDuplicate() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        ReadableBuffer duplicate = buffer.duplicate();
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], duplicate.get());
+        }
+
+        assertFalse(duplicate.hasRemaining());
+    }
+
+    @Test
+    public void testSlice() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        ReadableBuffer slice = buffer.slice();
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], slice.get());
+        }
+
+        assertFalse(slice.hasRemaining());
+    }
+
+    @Test
+    public void testLimit() {
+        byte[] data = new byte[] { 1, 2 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(data.length, buffer.limit());
+        buffer.limit(1);
+        assertEquals(1, buffer.limit());
+        assertEquals(1, buffer.get());
+        assertFalse(buffer.hasRemaining());
+
+        try {
+            buffer.get();
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testClear() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4};
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        byte[] target = new byte[data.length];
+
+        buffer.get(target);
+        assertFalse(buffer.hasRemaining());
+        assertArrayEquals(data, target);
+
+        try {
+            buffer.get(target);
+            fail("Should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        buffer.clear();
+        assertTrue(buffer.hasRemaining());
+        assertEquals(data.length, buffer.remaining());
+        buffer.get(target);
+        assertFalse(buffer.hasRemaining());
+        assertArrayEquals(data, target);
+    }
+
+    @Test
+    public void testRewind() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+
+        assertFalse(buffer.hasRemaining());
+        buffer.rewind();
+        assertTrue(buffer.hasRemaining());
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+    }
+
+    @Test
+    public void testReset() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        buffer.mark();
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+
+        assertFalse(buffer.hasRemaining());
+        buffer.reset();
+        assertTrue(buffer.hasRemaining());
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+    }
+
+    @Test
+    public void testGetPosition() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(buffer.position(), 0);
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(buffer.position(), i);
+            assertEquals(data[i], buffer.get());
+            assertEquals(buffer.position(), i + 1);
+        }
+    }
+
+    @Test
+    public void testSetPosition() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+
+        assertFalse(buffer.hasRemaining());
+        buffer.position(0);
+        assertTrue(buffer.hasRemaining());
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+    }
+
+    @Test
+    public void testFlip() {
+        byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(data);
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        buffer.mark();
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+
+        assertFalse(buffer.hasRemaining());
+        buffer.flip();
+        assertTrue(buffer.hasRemaining());
+
+        for (int i = 0; i < data.length; i++) {
+            assertEquals(data[i], buffer.get());
+        }
+    }
+
+    @Test
+    public void testReadUTF8() throws CharacterCodingException {
+        String testString = "test-string-1";
+        byte[] asUtf8bytes = testString.getBytes(StandardCharsets.UTF_8);
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(asUtf8bytes);
+
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(testString, buffer.readUTF8());
+    }
+
+    @Test
+    public void testReadString() throws CharacterCodingException {
+        String testString = "test-string-1";
+        byte[] asUtf8bytes = testString.getBytes(StandardCharsets.UTF_8);
+        ByteBuf byteBuffer = Unpooled.wrappedBuffer(asUtf8bytes);
+
+        NettyReadableBuffer buffer = new NettyReadableBuffer(byteBuffer);
+
+        assertEquals(testString, buffer.readString(StandardCharsets.UTF_8.newDecoder()));
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/NettyWritableBufferTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/NettyWritableBufferTest.java
new file mode 100644
index 0000000..36db410
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/NettyWritableBufferTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Tests for behavior of NettyWritableBuffer
+ */
+public class NettyWritableBufferTest {
+
+    @Test
+    public void testGetBuffer() {
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertSame(buffer, writable.getBuffer());
+    }
+
+    @Test
+    public void testLimit() {
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(buffer.capacity(), writable.limit());
+    }
+
+    @Test
+    public void testRemaining() {
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(buffer.maxCapacity(), writable.remaining());
+        writable.put((byte) 0);
+        assertEquals(buffer.maxCapacity() - 1, writable.remaining());
+    }
+
+    @Test
+    public void testHasRemaining() {
+        ByteBuf buffer = Unpooled.buffer(100, 100);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertTrue(writable.hasRemaining());
+        writable.put((byte) 0);
+        assertTrue(writable.hasRemaining());
+        buffer.writerIndex(buffer.maxCapacity());
+        assertFalse(writable.hasRemaining());
+    }
+
+    @Test
+    public void testGetPosition() {
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(0, writable.position());
+        writable.put((byte) 0);
+        assertEquals(1, writable.position());
+    }
+
+    @Test
+    public void testSetPosition() {
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(0, writable.position());
+        writable.position(1);
+        assertEquals(1, writable.position());
+    }
+
+    @Test
+    public void testPutByteBuffer() {
+        ByteBuffer input = ByteBuffer.allocate(1024);
+        input.put((byte) 1);
+        input.flip();
+
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(0, writable.position());
+        writable.put(input);
+        assertEquals(1, writable.position());
+    }
+
+    @Test
+    public void testPutByteBuf() {
+        ByteBuf input = Unpooled.buffer();
+        input.writeByte((byte) 1);
+
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(0, writable.position());
+        writable.put(input);
+        assertEquals(1, writable.position());
+    }
+
+    @Test
+    public void testPutString() {
+        String ascii = new String("ASCII");
+
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(0, writable.position());
+        writable.put(ascii);
+        assertEquals(ascii.length(), writable.position());
+        assertEquals(ascii, writable.getBuffer().toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testPutReadableBuffer() {
+        doPutReadableBufferTestImpl(true);
+        doPutReadableBufferTestImpl(false);
+    }
+
+    private void doPutReadableBufferTestImpl(boolean readOnly) {
+        ByteBuffer buf = ByteBuffer.allocate(1024);
+        buf.put((byte) 1);
+        buf.flip();
+        if (readOnly) {
+            buf = buf.asReadOnlyBuffer();
+        }
+
+        ReadableBuffer input = new ReadableBuffer.ByteBufferReader(buf);
+
+        if (readOnly) {
+            assertFalse(input.hasArray(), "Expected buffer not to hasArray()");
+        } else {
+            assertTrue(input.hasArray(), "Expected buffer to hasArray()");
+        }
+
+        ByteBuf buffer = Unpooled.buffer(1024);
+        NettyWritableBuffer writable = new NettyWritableBuffer(buffer);
+
+        assertEquals(0, writable.position());
+        writable.put(input);
+        assertEquals(1, writable.position());
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/WritableBufferTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/WritableBufferTest.java
new file mode 100644
index 0000000..620756c
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/buffer/WritableBufferTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for built in ByteBuffer wrapper of WritableBuffer
+ */
+public class WritableBufferTest {
+
+    @Test
+    public void testCreateAllocatedWrapper() {
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.allocate(10);
+
+        assertEquals(10, buffer.remaining());
+        assertEquals(0, buffer.position());
+        assertTrue(buffer.hasRemaining());
+    }
+
+    @Test
+    public void testCreateByteArrayWrapper() {
+        byte[] data = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(10, buffer.remaining());
+        assertEquals(0, buffer.position());
+        assertTrue(buffer.hasRemaining());
+    }
+
+    @Test
+    public void testLimit() {
+        ByteBuffer data = ByteBuffer.allocate(100);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(data.capacity(), buffer.limit());
+    }
+
+    @Test
+    public void testRemaining() {
+        ByteBuffer data = ByteBuffer.allocate(100);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(data.limit(), buffer.remaining());
+        buffer.put((byte) 0);
+        assertEquals(data.limit() - 1, buffer.remaining());
+    }
+
+    @Test
+    public void testHasRemaining() {
+        ByteBuffer data = ByteBuffer.allocate(100);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertTrue(buffer.hasRemaining());
+        buffer.put((byte) 0);
+        assertTrue(buffer.hasRemaining());
+        data.position(data.limit());
+        assertFalse(buffer.hasRemaining());
+    }
+
+    @Test
+    public void testEnsureRemainingThrowsWhenExpected() {
+        ByteBuffer data = ByteBuffer.allocate(100);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(data.capacity(), buffer.limit());
+        try {
+            buffer.ensureRemaining(1024);
+            fail("Should have thrown an error on request for more than is available.");
+        } catch (BufferOverflowException boe) {}
+
+        try {
+            buffer.ensureRemaining(-1);
+            fail("Should have thrown an error on request for negative space.");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testEnsureRemainingDefaultImplementation() {
+        WritableBuffer buffer = new DefaultWritableBuffer();
+
+        try {
+            buffer.ensureRemaining(1024);
+        } catch (IndexOutOfBoundsException iobe) {
+            fail("Should not have thrown an error on request for more than is available.");
+        }
+
+        try {
+            buffer.ensureRemaining(-1);
+        } catch (IllegalArgumentException iae) {
+            fail("Should not have thrown an error on request for negative space.");
+        }
+    }
+
+    @Test
+    public void testGetPosition() {
+        ByteBuffer data = ByteBuffer.allocate(100);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(0, buffer.position());
+        data.put((byte) 0);
+        assertEquals(1, buffer.position());
+    }
+
+    @Test
+    public void testSetPosition() {
+        ByteBuffer data = ByteBuffer.allocate(100);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(0, data.position());
+        buffer.position(1);
+        assertEquals(1, data.position());
+    }
+
+    @Test
+    public void testPutByteBuffer() {
+        ByteBuffer input = ByteBuffer.allocate(1024);
+        input.put((byte) 1);
+        input.flip();
+
+        ByteBuffer data = ByteBuffer.allocate(1024);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(0, buffer.position());
+        buffer.put(input);
+        assertEquals(1, buffer.position());
+    }
+
+    @Test
+    public void testPutString() {
+        String ascii = new String("ASCII");
+
+        ByteBuffer data = ByteBuffer.allocate(1024);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(0, buffer.position());
+        buffer.put(ascii);
+        assertEquals(ascii.length(), buffer.position());
+    }
+
+    @Test
+    public void testPutReadableBuffer() {
+        doPutReadableBufferTestImpl(true);
+        doPutReadableBufferTestImpl(false);
+    }
+
+    private void doPutReadableBufferTestImpl(boolean readOnly) {
+        ByteBuffer buf = ByteBuffer.allocate(1024);
+        buf.put((byte) 1);
+        buf.flip();
+        if (readOnly) {
+            buf = buf.asReadOnlyBuffer();
+        }
+
+        ReadableBuffer input = new ReadableBuffer.ByteBufferReader(buf);
+
+        if (readOnly) {
+            assertFalse(input.hasArray(), "Expected buffer not to hasArray()");
+        } else {
+            assertTrue(input.hasArray(), "Expected buffer to hasArray()");
+        }
+
+        ByteBuffer data = ByteBuffer.allocate(1024);
+        WritableBuffer buffer = WritableBuffer.ByteBufferWrapper.wrap(data);
+
+        assertEquals(0, buffer.position());
+        buffer.put(input);
+        assertEquals(1, buffer.position());
+    }
+
+    //----- WritableBuffer implementation with no default overrides ----------//
+
+    private static class DefaultWritableBuffer implements WritableBuffer {
+
+        private final WritableBuffer backing;
+
+        public DefaultWritableBuffer() {
+            backing = WritableBuffer.ByteBufferWrapper.allocate(1024);
+        }
+
+        @Override
+        public void put(byte b) {
+            backing.put(b);
+        }
+
+        @Override
+        public void putFloat(float f) {
+            backing.putFloat(f);
+        }
+
+        @Override
+        public void putDouble(double d) {
+            backing.putDouble(d);
+        }
+
+        @Override
+        public void put(byte[] src, int offset, int length) {
+            backing.put(src, offset, length);
+        }
+
+        @Override
+        public void putShort(short s) {
+            backing.putShort(s);
+        }
+
+        @Override
+        public void putInt(int i) {
+            backing.putInt(i);
+        }
+
+        @Override
+        public void putLong(long l) {
+            backing.putLong(l);
+        }
+
+        @Override
+        public boolean hasRemaining() {
+            return backing.hasRemaining();
+        }
+
+        @Override
+        public int remaining() {
+            return backing.remaining();
+        }
+
+        @Override
+        public int position() {
+            return backing.position();
+        }
+
+        @Override
+        public void position(int position) {
+            backing.position(position);
+        }
+
+        @Override
+        public void put(ByteBuffer payload) {
+            backing.put(payload);
+        }
+
+        @Override
+        public void put(ReadableBuffer payload) {
+            backing.put(payload);
+        }
+
+        @Override
+        public int limit() {
+            return backing.limit();
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/DataImplTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/DataImplTest.java
new file mode 100644
index 0000000..ca04788
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/DataImplTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
+import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedShort;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Attach;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Begin;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Open;
+import org.apache.qpid.protonj2.test.driver.codec.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
+import org.apache.qpid.protonj2.test.driver.codec.transport.SenderSettleMode;
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Test some basic operations of the Data type codec
+ */
+public class DataImplTest {
+
+    private final Codec codec = Codec.Factory.create();
+
+    @Test
+    public void testDecodeOpen() {
+        Open open = new Open();
+        open.setContainerId("test");
+        open.setHostname("localhost");
+
+        ByteBuf encoded = encodeProtonPerformative(open);
+        int expectedRead = encoded.readableBytes();
+
+        Codec codec = Codec.Factory.create();
+
+        assertEquals(expectedRead, codec.decode(encoded));
+
+        Open described = (Open) codec.getDescribedType();
+        assertNotNull(described);
+        assertEquals(Open.DESCRIPTOR_SYMBOL, described.getDescriptor());
+
+        assertEquals(open.getContainerId(), described.getContainerId());
+        assertEquals(open.getHostname(), described.getHostname());
+    }
+
+    @Test
+    public void testEncodeOpen() throws IOException {
+        Open open =new Open();
+        open.setContainerId("test");
+        open.setHostname("localhost");
+
+        Codec codec = Codec.Factory.create();
+
+        codec.putDescribedType(open);
+        ByteBuf encoded = Unpooled.buffer((int) codec.encodedSize());
+        codec.encode(encoded);
+
+        DescribedType decoded = decodeProtonPerformative(encoded);
+        assertNotNull(decoded);
+        assertTrue(decoded instanceof Open);
+
+        Open performative = (Open) decoded;
+        assertEquals(open.getContainerId(), performative.getContainerId());
+        assertEquals(open.getHostname(), performative.getHostname());
+    }
+
+    @Test
+    public void testDecodeBegin() {
+        Begin begin = new Begin();
+        begin.setHandleMax(UnsignedInteger.valueOf(512));
+        begin.setRemoteChannel(UnsignedShort.valueOf(1));
+
+        ByteBuf encoded = encodeProtonPerformative(begin);
+        int expectedRead = encoded.readableBytes();
+
+        Codec codec = Codec.Factory.create();
+
+        assertEquals(expectedRead, codec.decode(encoded));
+
+        Begin described = (Begin) codec.getDescribedType();
+        assertNotNull(described);
+        assertEquals(Begin.DESCRIPTOR_SYMBOL, described.getDescriptor());
+
+        assertEquals(described.getHandleMax(), UnsignedInteger.valueOf(512));
+        assertEquals(described.getRemoteChannel(), UnsignedShort.valueOf((short) 1));
+    }
+
+    @Test
+    public void testEncodeBegin() throws IOException {
+        Begin begin = new Begin();
+        begin.setHandleMax(UnsignedInteger.valueOf(512));
+        begin.setRemoteChannel(UnsignedShort.valueOf((short) 1));
+        begin.setIncomingWindow(UnsignedInteger.valueOf(2));
+        begin.setNextOutgoingId(UnsignedInteger.valueOf(2));
+        begin.setOutgoingWindow(UnsignedInteger.valueOf(3));
+
+        Codec codec = Codec.Factory.create();
+
+        codec.putDescribedType(begin);
+        ByteBuf encoded = Unpooled.buffer((int) codec.encodedSize());
+        codec.encode(encoded);
+
+        DescribedType decoded = decodeProtonPerformative(encoded);
+        assertNotNull(decoded);
+        assertTrue(decoded instanceof Begin);
+
+        Begin performative = (Begin) decoded;
+        assertEquals(performative.getHandleMax(), UnsignedInteger.valueOf(512));
+        assertEquals(performative.getRemoteChannel(), UnsignedShort.valueOf((short) 1));
+    }
+
+    @Test
+    public void testDecodeAttach() {
+        Attach attach = new Attach();
+        attach.setName("test");
+        attach.setHandle(UnsignedInteger.valueOf(1));
+        attach.setRole(Role.SENDER);
+        attach.setSenderSettleMode(SenderSettleMode.MIXED);
+        attach.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+        attach.setSource(new Source());
+        attach.setTarget(new Target());
+
+        ByteBuf encoded = encodeProtonPerformative(attach);
+        int expectedRead = encoded.readableBytes();
+
+        Codec codec = Codec.Factory.create();
+
+        assertEquals(expectedRead, codec.decode(encoded));
+
+        Attach described = (Attach) codec.getDescribedType();
+        assertNotNull(described);
+        assertEquals(Attach.DESCRIPTOR_SYMBOL, described.getDescriptor());
+
+        assertEquals(described.getHandle(), UnsignedInteger.valueOf(1));
+        assertEquals(described.getName(), "test");
+    }
+
+    @Test
+    public void testEncodeAttach() throws IOException {
+        Attach attach = new Attach();
+        attach.setName("test");
+        attach.setHandle(UnsignedInteger.valueOf(1));
+        attach.setRole(Role.SENDER.getValue());
+        attach.setSenderSettleMode(SenderSettleMode.MIXED.getValue());
+        attach.setReceiverSettleMode(ReceiverSettleMode.FIRST.getValue());
+        attach.setSource(new Source());
+        attach.setTarget(new Target());
+
+        Codec codec = Codec.Factory.create();
+
+        codec.putDescribedType(attach);
+        ByteBuf encoded = Unpooled.buffer((int) codec.encodedSize());
+        codec.encode(encoded);
+
+        DescribedType decoded = decodeProtonPerformative(encoded);
+        assertNotNull(decoded);
+        assertTrue(decoded instanceof Attach);
+
+        Attach performative = (Attach) decoded;
+        assertEquals(performative.getHandle(), UnsignedInteger.valueOf(1));
+        assertEquals(performative.getName(), "test");
+    }
+
+    private DescribedType decodeProtonPerformative(ByteBuf buffer) throws IOException {
+        DescribedType performative = null;
+
+        try {
+            codec.decode(buffer);
+        } catch (Exception e) {
+            throw new AssertionError("Decoder failed reading remote input:", e);
+        }
+
+        Codec.DataType dataType = codec.type();
+        if (dataType != Codec.DataType.DESCRIBED) {
+            throw new IllegalArgumentException(
+                "Frame body type expected to be " + Codec.DataType.DESCRIBED + " but was: " + dataType);
+        }
+
+        try {
+            performative = codec.getDescribedType();
+        } finally {
+            codec.clear();
+        }
+
+        return performative;
+    }
+
+    private ByteBuf encodeProtonPerformative(DescribedType performative) {
+        ByteBuf buffer = Unpooled.buffer();
+
+        if (performative != null) {
+            try {
+                codec.putDescribedType(performative);
+                codec.encode(buffer);
+            } finally {
+                codec.clear();
+            }
+        }
+
+        return buffer;
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/BinaryTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/BinaryTest.java
new file mode 100644
index 0000000..395fb9c
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/BinaryTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+public class BinaryTest {
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testNotEqualsWithDifferentTypeObject() {
+        Binary binary = createSteppedValueBinary(10);
+
+        assertFalse(binary.equals("not-a-Binary"), "Objects should not be equal with different type");
+    }
+
+    @Test
+    public void testEqualsWithItself() {
+        Binary binary = createSteppedValueBinary(10);
+
+        assertTrue(binary.equals(binary), "Object should be equal to itself");
+    }
+
+    @Test
+    public void testEqualsWithDifferentBinaryOfSameLengthAndContent() {
+        int length = 10;
+        Binary bin1 = createSteppedValueBinary(length);
+        Binary bin2 = createSteppedValueBinary(length);
+
+        assertTrue(bin1.equals(bin2), "Objects should be equal");
+        assertTrue(bin2.equals(bin1), "Objects should be equal");
+    }
+
+    @Test
+    public void testEqualsWithDifferentLengthBinaryOfDifferentBytes() {
+        int length1 = 10;
+        Binary bin1 = createSteppedValueBinary(length1);
+        Binary bin2 = createSteppedValueBinary(length1 + 1);
+
+        assertFalse(bin1.equals(bin2), "Objects should not be equal");
+        assertFalse(bin2.equals(bin1), "Objects should not be equal");
+    }
+
+    @Test
+    public void testEqualsWithDifferentLengthBinaryOfSameByte() {
+        Binary bin1 = createNewRepeatedValueBinary(10, (byte) 1);
+        Binary bin2 = createNewRepeatedValueBinary(123, (byte) 1);
+
+        assertFalse(bin1.equals(bin2), "Objects should not be equal");
+        assertFalse(bin2.equals(bin1), "Objects should not be equal");
+    }
+
+    @Test
+    public void testEqualsWithDifferentContentBinary() {
+        int length = 10;
+        Binary bin1 = createNewRepeatedValueBinary(length, (byte) 1);
+
+        Binary bin2 = createNewRepeatedValueBinary(length, (byte) 1);
+        bin2.getArray()[5] = (byte) 0;
+
+        assertFalse(bin1.equals(bin2), "Objects should not be equal");
+        assertFalse(bin2.equals(bin1), "Objects should not be equal");
+    }
+
+    private Binary createSteppedValueBinary(int length) {
+        byte[] bytes = new byte[length];
+        for (int i = 0; i < length; i++) {
+            bytes[i] = (byte) (length - i);
+        }
+
+        return new Binary(bytes);
+    }
+
+    private Binary createNewRepeatedValueBinary(int length, byte repeatedByte) {
+        byte[] bytes = new byte[length];
+        Arrays.fill(bytes, repeatedByte);
+
+        return new Binary(bytes);
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/SymbolTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/SymbolTest.java
new file mode 100644
index 0000000..ec5064d
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/SymbolTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+public class SymbolTest {
+
+    private final String LARGE_SYMBOL_VALUIE = "Large String: " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog.";
+
+    @Test
+    public void testGetSymbolWithNullString() {
+        assertNull(Symbol.getSymbol((String) null));
+    }
+
+    @Test
+    public void testGetSymbolWithNullBuffer() {
+        assertNull(Symbol.getSymbol((ByteBuffer) null));
+    }
+
+    @Test
+    public void testGetSymbolWithEmptyString() {
+        assertNotNull(Symbol.getSymbol(""));
+        assertSame(Symbol.getSymbol(""), Symbol.getSymbol(""));
+    }
+
+    @Test
+    public void testGetSymbolWithEmptyBuffer() {
+        assertNotNull(Symbol.getSymbol(ByteBuffer.allocate(0)));
+        assertSame(Symbol.getSymbol(ByteBuffer.allocate(0)),
+                   Symbol.getSymbol(ByteBuffer.allocate(0)));
+    }
+
+    @Test
+    public void testCompareTo() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+        String symbolString3 = "Symbol-3";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+        Symbol symbol3 = Symbol.valueOf(symbolString3);
+
+        assertEquals(0, symbol1.compareTo(symbol1));
+        assertEquals(0, symbol2.compareTo(symbol2));
+        assertEquals(0, symbol3.compareTo(symbol3));
+
+        assertTrue(symbol2.compareTo(symbol1) > 0);
+        assertTrue(symbol3.compareTo(symbol1) > 0);
+        assertTrue(symbol3.compareTo(symbol2) > 0);
+
+        assertTrue(symbol1.compareTo(symbol2) < 0);
+        assertTrue(symbol1.compareTo(symbol3) < 0);
+        assertTrue(symbol2.compareTo(symbol3) < 0);
+    }
+
+    @Test
+    public void testEquals() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+        String symbolString3 = "Symbol-3";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+        Symbol symbol3 = Symbol.valueOf(symbolString3);
+
+        assertNotEquals(symbol1, symbol2);
+
+        assertEquals(symbolString1, symbol1.toString());
+        assertEquals(symbolString2, symbol2.toString());
+        assertEquals(symbolString3, symbol3.toString());
+
+        assertNotEquals(symbol1, symbol2);
+        assertNotEquals(symbol2, symbol3);
+        assertNotEquals(symbol3, symbol1);
+
+        assertNotEquals(symbolString1, symbol1);
+        assertNotEquals(symbolString2, symbol2);
+        assertNotEquals(symbolString3, symbol3);
+    }
+
+    @Test
+    public void testHashcode() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+
+        assertNotEquals(symbol1, symbol2);
+        assertNotEquals(symbol1.hashCode(), symbol2.hashCode());
+
+        assertEquals(symbol1.hashCode(), Symbol.valueOf(symbolString1).hashCode());
+        assertEquals(symbol2.hashCode(), Symbol.valueOf(symbolString2).hashCode());
+    }
+
+    @Test
+    public void testValueOf() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+
+        assertNotEquals(symbol1, symbol2);
+
+        assertEquals(symbolString1, symbol1.toString());
+        assertEquals(symbolString2, symbol2.toString());
+    }
+
+    @Test
+    public void testValueOfProducesSingleton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString);
+        Symbol symbol2 = Symbol.valueOf(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+    }
+
+    @Test
+    public void testGetSymbol() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+
+        Symbol symbol1 = Symbol.getSymbol(symbolString1);
+        Symbol symbol2 = Symbol.getSymbol(symbolString2);
+
+        assertNotEquals(symbol1, symbol2);
+
+        assertEquals(symbolString1, symbol1.toString());
+        assertEquals(symbolString2, symbol2.toString());
+    }
+
+    @Test
+    public void testGetSymbolProducesSingleton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.getSymbol(symbolString);
+        Symbol symbol2 = Symbol.getSymbol(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+    }
+
+    @Test
+    public void testGetSymbolAndValueOfProduceSingleton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString);
+        Symbol symbol2 = Symbol.getSymbol(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+    }
+
+    @Test
+    public void testToStringProducesSingelton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.getSymbol(symbolString);
+        Symbol symbol2 = Symbol.getSymbol(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+        assertSame(symbol1.toString(), symbol2.toString());
+    }
+
+    @Test
+    public void testLrageSymbolNotCached() {
+        Symbol symbol1 = Symbol.valueOf(LARGE_SYMBOL_VALUIE);
+        Symbol symbol2 = Symbol.getSymbol(ByteBuffer.wrap(LARGE_SYMBOL_VALUIE.getBytes(StandardCharsets.US_ASCII)));
+
+        assertNotSame(symbol1, symbol2);
+        assertNotSame(symbol1.toString(), symbol2.toString());
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedByteTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedByteTest.java
new file mode 100644
index 0000000..e527e9c
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedByteTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedByteTest {
+
+    @Test
+    public void testToString() {
+        assertEquals("0", UnsignedByte.valueOf((byte) 0).toString());
+        assertEquals("255", UnsignedByte.valueOf((byte) 255).toString());
+        assertEquals("127", UnsignedByte.valueOf((byte) 127).toString());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedByte ubyte1 = UnsignedByte.valueOf((byte) 1);
+        UnsignedByte ubyte2 = UnsignedByte.valueOf((byte) 2);
+
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1.hashCode(), ubyte2.hashCode());
+
+        assertEquals(ubyte1.hashCode(), UnsignedByte.valueOf((byte) 1).hashCode());
+        assertEquals(ubyte2.hashCode(), UnsignedByte.valueOf((byte) 2).hashCode());
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedByte.valueOf((byte) 0).shortValue());
+        assertEquals((short) 255, UnsignedByte.valueOf((byte) 255).shortValue());
+        assertEquals((short) 1, UnsignedByte.valueOf((byte) 1).shortValue());
+        assertEquals((short) 127, UnsignedByte.valueOf((byte) 127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedByte.valueOf((byte) 0).intValue());
+        assertEquals(255, UnsignedByte.valueOf((byte) 255).intValue());
+        assertEquals(1, UnsignedByte.valueOf((byte) 1).intValue());
+        assertEquals(127, UnsignedByte.valueOf((byte) 127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        assertEquals(0l, UnsignedByte.valueOf((byte) 0).longValue());
+        assertEquals(255l, UnsignedByte.valueOf((byte) 255).longValue());
+        assertEquals(1l, UnsignedByte.valueOf((byte) 1).longValue());
+        assertEquals(127l, UnsignedByte.valueOf((byte) 127).longValue());
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo((byte) 255) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo((byte) 0) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 127).compareTo((byte) 126) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 32).compareTo((byte) 64) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo((byte) 127) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 126).compareTo((byte) 255) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo((byte) 0) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo((byte) 255) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedByte() {
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo(UnsignedByte.valueOf((byte) 255)) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo(UnsignedByte.valueOf((byte) 0)) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 127).compareTo(UnsignedByte.valueOf((byte) 126)) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 32).compareTo(UnsignedByte.valueOf((byte) 64)) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo(UnsignedByte.valueOf((byte) 127)) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 126).compareTo(UnsignedByte.valueOf((byte) 255)) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo(UnsignedByte.valueOf((byte) 0)) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo(UnsignedByte.valueOf((byte) 255)) < 0);
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedByte.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithTextThrowsNFE() throws Exception {
+        try {
+            UnsignedByte.valueOf("TEST");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithOutOfRangeValueThrowsNFE() throws Exception {
+        try {
+            UnsignedByte.valueOf("256");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testToUnsignedShortValueFromByte() {
+        short shortValue = Byte.MAX_VALUE + 1;
+
+        assertEquals(0, UnsignedByte.toUnsignedShort((byte) 0));
+        assertEquals(255, UnsignedByte.toUnsignedShort((byte) 255));
+        assertEquals(1, UnsignedByte.toUnsignedShort((byte) 1));
+        assertEquals(127, UnsignedByte.toUnsignedShort((byte) 127));
+        assertEquals(shortValue, UnsignedByte.toUnsignedShort((byte) (Byte.MAX_VALUE + 1)));
+    }
+
+    @Test
+    public void testToUnsignedIntValueFromByte() {
+        int intValue = Byte.MAX_VALUE + 1;
+
+        assertEquals(0, UnsignedByte.toUnsignedInt((byte) 0));
+        assertEquals(255, UnsignedByte.toUnsignedInt((byte) 255));
+        assertEquals(1, UnsignedByte.toUnsignedInt((byte) 1));
+        assertEquals(127, UnsignedByte.toUnsignedInt((byte) 127));
+        assertEquals(intValue, UnsignedByte.toUnsignedInt((byte) (Byte.MAX_VALUE + 1)));
+    }
+
+    @Test
+    public void testToUnsignedLongValueFromByte() {
+        long longValue = (long) Byte.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedByte.toUnsignedLong((byte) 0));
+        assertEquals(255l, UnsignedByte.toUnsignedLong((byte) 255));
+        assertEquals(1l, UnsignedByte.toUnsignedLong((byte) 1));
+        assertEquals(127l, UnsignedByte.toUnsignedLong((byte) 127));
+        assertEquals(longValue, UnsignedByte.toUnsignedLong((byte) (Byte.MAX_VALUE + 1)));
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedIntegerTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedIntegerTest.java
new file mode 100644
index 0000000..d19b1ad
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedIntegerTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedIntegerTest {
+
+    @Test
+    public void testToString() {
+        assertEquals("0",  UnsignedInteger.valueOf(0).toString());
+        assertEquals("65535", UnsignedInteger.valueOf(65535).toString());
+        assertEquals("127", UnsignedInteger.valueOf(127).toString());
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testEquals() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(2);
+
+        assertEquals(uint1, uint1);
+        assertEquals(uint1, UnsignedInteger.valueOf(1));
+        assertEquals(uint1, UnsignedInteger.valueOf("1"));
+        assertFalse(uint1.equals(uint2));
+        assertNotEquals(uint1.hashCode(), uint2.hashCode());
+
+        assertEquals(uint1.hashCode(), UnsignedInteger.valueOf(1).hashCode());
+        assertEquals(uint2.hashCode(), UnsignedInteger.valueOf(2).hashCode());
+
+        assertFalse(uint1.equals(null));
+        assertFalse(uint1.equals("test"));
+    }
+
+    @Test
+    public void testValueOfFromLong() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1l);
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(longValue);
+
+        assertEquals(1, uint1.intValue());
+        assertEquals(longValue, uint2.longValue());
+    }
+
+    @Test
+    public void testValueOfFromString() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        UnsignedInteger uint1 = UnsignedInteger.valueOf("1");
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(String.valueOf(longValue));
+
+        assertEquals(1, uint1.intValue());
+        assertEquals(longValue, uint2.longValue());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(2);
+
+        assertNotEquals(uint1, uint2);
+        assertNotEquals(uint1.hashCode(), uint2.hashCode());
+
+        assertEquals(uint1.hashCode(), UnsignedInteger.valueOf(1).hashCode());
+        assertEquals(uint2.hashCode(), UnsignedInteger.valueOf(2).hashCode());
+    }
+
+    @Test
+    public void testAdd() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger result = uint1.add(uint1);
+        assertEquals(2, result.intValue());
+    }
+
+    @Test
+    public void testSubtract() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger result = uint1.subtract(uint1);
+        assertEquals(0, result.intValue());
+    }
+
+    @Test
+    public void testCompareToPrimitiveInt() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        assertEquals(0, uint1.compareTo(1));
+        assertEquals(1, uint1.compareTo(0));
+        assertEquals(-1, uint1.compareTo(2));
+    }
+
+    @Test
+    public void testCompareToPrimitiveLong() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        assertEquals(0, uint1.compareTo(1l));
+        assertEquals(1, uint1.compareTo(0l));
+        assertEquals(-1, uint1.compareTo(2l));
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedInteger.valueOf(0).shortValue());
+        assertEquals((short) 65535, UnsignedInteger.valueOf(65535).shortValue());
+        assertEquals((short) 1, UnsignedInteger.valueOf(1).shortValue());
+        assertEquals((short) 127, UnsignedInteger.valueOf(127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedInteger.valueOf(0).intValue());
+        assertEquals(65535, UnsignedInteger.valueOf(65535).intValue());
+        assertEquals(1, UnsignedInteger.valueOf(1).intValue());
+        assertEquals(127, UnsignedInteger.valueOf(127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedInteger.valueOf(0).longValue());
+        assertEquals(65535l, UnsignedInteger.valueOf(65535).longValue());
+        assertEquals(1l, UnsignedInteger.valueOf(1).longValue());
+        assertEquals(127l, UnsignedInteger.valueOf(127).longValue());
+        assertEquals(longValue, UnsignedInteger.valueOf(Integer.MAX_VALUE + 1).longValue());
+    }
+
+    @Test
+    public void testToUnsignedLongValueFromInt() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedInteger.toUnsignedLong(0));
+        assertEquals(65535l, UnsignedInteger.toUnsignedLong(65535));
+        assertEquals(1l, UnsignedInteger.toUnsignedLong(1));
+        assertEquals(127l, UnsignedInteger.toUnsignedLong(127));
+        assertEquals(longValue, UnsignedInteger.toUnsignedLong(Integer.MAX_VALUE + 1));
+    }
+
+    @Test
+    public void testFloatValue() {
+        assertEquals(0.0f, UnsignedInteger.valueOf(0).floatValue(), 0.0f);
+        assertEquals(65535.0f, UnsignedInteger.valueOf(65535).floatValue(), 0.0f);
+        assertEquals(1.0f, UnsignedInteger.valueOf(1).floatValue(), 0.0f);
+        assertEquals(127.0f, UnsignedInteger.valueOf(127).floatValue(), 0.0f);
+    }
+
+    @Test
+    public void testDoubleValue() {
+        assertEquals(0.0, UnsignedInteger.valueOf(0).doubleValue(), 0.0);
+        assertEquals(65535.0, UnsignedInteger.valueOf(65535).doubleValue(), 0.0);
+        assertEquals(1.0, UnsignedInteger.valueOf(1).doubleValue(), 0.0);
+        assertEquals(127.0, UnsignedInteger.valueOf(127).doubleValue(), 0.0);
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedInteger.valueOf(255).compareTo(255) == 0);
+        assertTrue(UnsignedInteger.valueOf(0).compareTo(0) == 0);
+        assertTrue(UnsignedInteger.valueOf(127).compareTo(126) > 0);
+        assertTrue(UnsignedInteger.valueOf(32).compareTo(64) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedInteger() {
+        assertTrue(UnsignedInteger.valueOf(65535).compareTo(UnsignedInteger.valueOf(65535)) == 0);
+        assertTrue(UnsignedInteger.valueOf(0).compareTo(UnsignedInteger.valueOf(0)) == 0);
+        assertTrue(UnsignedInteger.valueOf(127).compareTo(UnsignedInteger.valueOf(126)) > 0);
+        assertTrue(UnsignedInteger.valueOf(32).compareTo(UnsignedInteger.valueOf(64)) < 0);
+    }
+
+    @Test
+    public void testCompareToIntInt() {
+        assertTrue(UnsignedInteger.compare(65536, 65536) == 0);
+        assertTrue(UnsignedInteger.compare(0, 0) == 0);
+        assertTrue(UnsignedInteger.compare(1, 2) < 0);
+        assertTrue(UnsignedInteger.compare(127, 32) > 0);
+    }
+
+    @Test
+    public void testCompareToLongLong() {
+        assertTrue(UnsignedInteger.compare(65536l, 65536l) == 0);
+        assertTrue(UnsignedInteger.compare(0l, 0l) == 0);
+        assertTrue(UnsignedInteger.compare(1l, 2l) < 0);
+        assertTrue(UnsignedInteger.compare(127l, 32l) > 0);
+    }
+
+    @Test
+    public void testValueOfLongWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf(-1l);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfLongWithLargeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf(Long.MAX_VALUE);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithTextThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf("TEST");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithOutOfRangeValueThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf("" + Long.MAX_VALUE);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedLongTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedLongTest.java
new file mode 100644
index 0000000..a792b9b
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedLongTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.math.BigInteger;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedLongTest {
+
+    private static final byte[] TWO_TO_64_PLUS_ONE_BYTES =
+        new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 1 };
+    private static final byte[] TWO_TO_64_MINUS_ONE_BYTES =
+        new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 };
+
+    @Test
+    public void testToString() {
+        assertEquals("0",  UnsignedLong.valueOf(0).toString());
+        assertEquals("65535", UnsignedLong.valueOf(65535).toString());
+        assertEquals("127", UnsignedLong.valueOf(127).toString());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedLong ubyte1 = UnsignedLong.valueOf((short) 1);
+        UnsignedLong ubyte2 = UnsignedLong.valueOf((short) 2);
+
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1.hashCode(), ubyte2.hashCode());
+
+        assertEquals(ubyte1.hashCode(), UnsignedLong.valueOf((short) 1).hashCode());
+        assertEquals(ubyte2.hashCode(), UnsignedLong.valueOf((short) 2).hashCode());
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedLong.valueOf(0).shortValue());
+        assertEquals((short) 65535, UnsignedLong.valueOf(65535).shortValue());
+        assertEquals((short) 1, UnsignedLong.valueOf(1).shortValue());
+        assertEquals((short) 127, UnsignedLong.valueOf(127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedLong.valueOf(0).intValue());
+        assertEquals(65535, UnsignedLong.valueOf(65535).intValue());
+        assertEquals(1, UnsignedLong.valueOf(1).intValue());
+        assertEquals(127, UnsignedLong.valueOf(127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        assertEquals(0l, UnsignedLong.valueOf(0).longValue());
+        assertEquals(65535l, UnsignedLong.valueOf(65535).longValue());
+        assertEquals(1l, UnsignedLong.valueOf(1).longValue());
+        assertEquals(127l, UnsignedLong.valueOf(127).longValue());
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedLong.valueOf(255).compareTo(255) == 0);
+        assertTrue(UnsignedLong.valueOf(0).compareTo(0) == 0);
+        assertTrue(UnsignedLong.valueOf(127).compareTo(126) > 0);
+        assertTrue(UnsignedLong.valueOf(32).compareTo(64) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedInteger() {
+        assertTrue(UnsignedLong.valueOf(65535).compareTo(UnsignedLong.valueOf(65535)) == 0);
+        assertTrue(UnsignedLong.valueOf(0).compareTo(UnsignedLong.valueOf(0)) == 0);
+        assertTrue(UnsignedLong.valueOf(127).compareTo(UnsignedLong.valueOf(126)) > 0);
+        assertTrue(UnsignedLong.valueOf(32).compareTo(UnsignedLong.valueOf(64)) < 0);
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedLong.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfBigIntegerWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedLong.valueOf(BigInteger.valueOf(-1L));
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValuesOfStringWithinRangeSucceed() throws Exception {
+        // check 0 (min) to confirm success
+        UnsignedLong min = UnsignedLong.valueOf("0");
+        assertEquals(0, min.longValue(), "unexpected value");
+
+        // check 2^64 -1 (max) to confirm success
+        BigInteger onLimit = new BigInteger(TWO_TO_64_MINUS_ONE_BYTES);
+        String onlimitString = onLimit.toString();
+        UnsignedLong max = UnsignedLong.valueOf(onlimitString);
+        assertEquals(onLimit, max.bigIntegerValue(), "unexpected value");
+    }
+
+    @Test
+    public void testValuesOfBigIntegerWithinRangeSucceed() throws Exception {
+        // check 0 (min) to confirm success
+        UnsignedLong min = UnsignedLong.valueOf(BigInteger.ZERO);
+        assertEquals(0, min.longValue(), "unexpected value");
+
+        // check 2^64 -1 (max) to confirm success
+        BigInteger onLimit = new BigInteger(TWO_TO_64_MINUS_ONE_BYTES);
+        UnsignedLong max = UnsignedLong.valueOf(onLimit);
+        assertEquals(onLimit, max.bigIntegerValue(), "unexpected value");
+
+        // check Long.MAX_VALUE to confirm success
+        UnsignedLong longMax = UnsignedLong.valueOf(BigInteger.valueOf(Long.MAX_VALUE));
+        assertEquals(Long.MAX_VALUE, longMax.longValue(), "unexpected value");
+    }
+
+    @Test
+    public void testValueOfStringAboveMaxValueThrowsNFE() throws Exception {
+        // 2^64 + 1 (value 2 over max)
+        BigInteger aboveLimit = new BigInteger(TWO_TO_64_PLUS_ONE_BYTES);
+        try {
+            UnsignedLong.valueOf(aboveLimit.toString());
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+
+        // 2^64 (value 1 over max)
+        aboveLimit = aboveLimit.subtract(BigInteger.ONE);
+        try {
+            UnsignedLong.valueOf(aboveLimit.toString());
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfBigIntegerAboveMaxValueThrowsNFE() throws Exception {
+        // 2^64 + 1 (value 2 over max)
+        BigInteger aboveLimit = new BigInteger(TWO_TO_64_PLUS_ONE_BYTES);
+        try {
+            UnsignedLong.valueOf(aboveLimit);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+
+        // 2^64 (value 1 over max)
+        aboveLimit = aboveLimit.subtract(BigInteger.ONE);
+        try {
+            UnsignedLong.valueOf(aboveLimit);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedShortTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedShortTest.java
new file mode 100644
index 0000000..6ae7369
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/primitives/UnsignedShortTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedShortTest {
+
+    @Test
+    public void testToString() {
+        assertEquals("0",  UnsignedShort.valueOf((short) 0).toString());
+        assertEquals("65535", UnsignedShort.valueOf((short) 65535).toString());
+        assertEquals("127", UnsignedShort.valueOf((short) 127).toString());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedShort ubyte1 = UnsignedShort.valueOf((short) 1);
+        UnsignedShort ubyte2 = UnsignedShort.valueOf((short) 2);
+
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1.hashCode(), ubyte2.hashCode());
+
+        assertEquals(ubyte1.hashCode(), UnsignedShort.valueOf((short) 1).hashCode());
+        assertEquals(ubyte2.hashCode(), UnsignedShort.valueOf((short) 2).hashCode());
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedShort.valueOf((short) 0).shortValue());
+        assertEquals((short) 65535, UnsignedShort.valueOf((short) 65535).shortValue());
+        assertEquals((short) 1, UnsignedShort.valueOf((short) 1).shortValue());
+        assertEquals((short) 127, UnsignedShort.valueOf((short) 127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedShort.valueOf((short) 0).intValue());
+        assertEquals(65535, UnsignedShort.valueOf((short) 65535).intValue());
+        assertEquals(1, UnsignedShort.valueOf((short) 1).intValue());
+        assertEquals(127, UnsignedShort.valueOf((short) 127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        assertEquals(0l, UnsignedShort.valueOf((short) 0).longValue());
+        assertEquals(65535l, UnsignedShort.valueOf((short) 65535).longValue());
+        assertEquals(1l, UnsignedShort.valueOf((short) 1).longValue());
+        assertEquals(127l, UnsignedShort.valueOf((short) 127).longValue());
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedShort.valueOf((short) 255).compareTo((short) 255) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 0).compareTo((short) 0) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 127).compareTo((short) 126) > 0);
+        assertTrue(UnsignedShort.valueOf((short) 32).compareTo((short) 64) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedShort() {
+        assertTrue(UnsignedShort.valueOf((short) 65535).compareTo(UnsignedShort.valueOf((short) 65535)) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 0).compareTo(UnsignedShort.valueOf((short) 0)) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 127).compareTo(UnsignedShort.valueOf((short) 126)) > 0);
+        assertTrue(UnsignedShort.valueOf((short) 32).compareTo(UnsignedShort.valueOf((short) 64)) < 0);
+    }
+
+    @Test
+    public void testCompareToIntInt() {
+        assertTrue(UnsignedShort.compare((short) 65536, (short) 65536) == 0);
+        assertTrue(UnsignedShort.compare((short) 0, (short) 0) == 0);
+        assertTrue(UnsignedShort.compare((short) 1, (short) 2) < 0);
+        assertTrue(UnsignedShort.compare((short) 127, (short) 32) > 0);
+    }
+
+    @Test
+    public void testValueOfIntWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf(-1);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfIntWithLargeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf(Integer.MAX_VALUE);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithTextThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf("TEST");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithOutOfRangeValueThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf("65536");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testToUnsignedIntValueFromShort() {
+        long longValue = (long) Short.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedShort.toUnsignedInt((short) 0));
+        assertEquals(65535l, UnsignedShort.toUnsignedInt((short) 65535));
+        assertEquals(1l, UnsignedShort.toUnsignedInt((short) 1));
+        assertEquals(127l, UnsignedShort.toUnsignedInt((short) 127));
+        assertEquals(longValue, UnsignedShort.toUnsignedInt((short) (Short.MAX_VALUE + 1)));
+    }
+
+    @Test
+    public void testToUnsignedLongValueFromShort() {
+        long longValue = (long) Short.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedShort.toUnsignedLong((short) 0));
+        assertEquals(65535l, UnsignedShort.toUnsignedLong((short) 65535));
+        assertEquals(1l, UnsignedShort.toUnsignedLong((short) 1));
+        assertEquals(127l, UnsignedShort.toUnsignedLong((short) 127));
+        assertEquals(longValue, UnsignedShort.toUnsignedLong((short) (Short.MAX_VALUE + 1)));
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/transport/AMQPHeaderTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/transport/AMQPHeaderTest.java
new file mode 100644
index 0000000..707b4d2
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/codec/transport/AMQPHeaderTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader.HeaderHandler;
+import org.junit.jupiter.api.Test;
+
+public class AMQPHeaderTest {
+
+    @Test
+    public void testDefaultCreate() {
+        AMQPHeader header = new AMQPHeader();
+
+        assertEquals(AMQPHeader.getAMQPHeader(), header);
+        assertFalse(header.isSaslHeader());
+        assertEquals(0, header.getProtocolId());
+        assertEquals(1, header.getMajor());
+        assertEquals(0, header.getMinor());
+        assertEquals(0, header.getRevision());
+        assertTrue(header.hasValidPrefix());
+    }
+
+    @Test
+    public void testToArray() {
+        AMQPHeader header = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        byte[] array = header.toArray();
+
+        assertArrayEquals(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0}, array);
+    }
+
+    @Test
+    public void testToByteBuffer() {
+        AMQPHeader header = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        ByteBuffer byteBuffer = header.toByteBuffer();
+
+        assertArrayEquals(header.toArray(), byteBuffer.array());
+    }
+
+    @Test
+    public void testCreateFromBufferWithoutValidation() {
+        AMQPHeader invalid = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 4, 1, 0, 0}, false);
+
+        assertEquals(4, invalid.getByteAt(4));
+        assertEquals(4, invalid.getProtocolId());
+    }
+
+    @Test
+    public void testCreateFromBufferWithoutValidationFailsWithToLargeInput() {
+        assertThrows(IndexOutOfBoundsException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 4, 1, 0, 0, 0}, false));
+    }
+
+    @Test
+    public void testGetBuffer() {
+        AMQPHeader header = new AMQPHeader();
+
+        assertNotNull(header.getBuffer());
+        byte[] buffer = header.getBuffer();
+
+        buffer[0] = 'B';
+
+        assertEquals('A', header.getByteAt(0));
+    }
+
+    @Test
+    public void testHashCode() {
+        AMQPHeader defaultCtor = new AMQPHeader();
+        AMQPHeader byteCtor = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader byteCtorSasl = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 3, 1, 0, 0});
+
+        assertEquals(defaultCtor.hashCode(), byteCtor.hashCode());
+        assertEquals(defaultCtor.hashCode(), AMQPHeader.getAMQPHeader().hashCode());
+        assertEquals(byteCtor.hashCode(), AMQPHeader.getAMQPHeader().hashCode());
+        assertEquals(byteCtorSasl.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+        assertNotEquals(byteCtor.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+        assertNotEquals(defaultCtor.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+        assertEquals(byteCtorSasl.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+    }
+
+    @Test
+    public void testIsTypeMethods() {
+        AMQPHeader defaultCtor = new AMQPHeader();
+        AMQPHeader byteCtor = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader byteCtorSasl = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 3, 1, 0, 0});
+
+        assertFalse(defaultCtor.isSaslHeader());
+        assertFalse(byteCtor.isSaslHeader());
+        assertTrue(byteCtorSasl.isSaslHeader());
+        assertFalse(AMQPHeader.getAMQPHeader().isSaslHeader());
+        assertTrue(AMQPHeader.getSASLHeader().isSaslHeader());
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testEquals() {
+        AMQPHeader defaultCtor = new AMQPHeader();
+        AMQPHeader byteCtor = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader byteCtorSasl = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 3, 1, 0, 0});
+
+        assertEquals(defaultCtor, defaultCtor);
+        assertEquals(defaultCtor, byteCtor);
+        assertEquals(byteCtor, byteCtor);
+        assertEquals(defaultCtor, AMQPHeader.getAMQPHeader());
+        assertEquals(byteCtor, AMQPHeader.getAMQPHeader());
+        assertEquals(byteCtorSasl, AMQPHeader.getSASLHeader());
+        assertNotEquals(byteCtor, AMQPHeader.getSASLHeader());
+        assertNotEquals(defaultCtor, AMQPHeader.getSASLHeader());
+        assertEquals(byteCtorSasl, AMQPHeader.getSASLHeader());
+
+        assertFalse(AMQPHeader.getSASLHeader().equals(null));
+        assertFalse(AMQPHeader.getSASLHeader().equals(Boolean.TRUE));
+    }
+
+    @Test
+    public void testToStringOnDefault() {
+        AMQPHeader header = new AMQPHeader();
+        assertTrue(header.toString().startsWith("AMQP"));
+    }
+
+    @Test
+    public void testValidateByteWithValidHeaderBytes() {
+        byte[] bytes = AMQPHeader.getAMQPHeader().toArray();
+
+        for (int i = 0; i < AMQPHeader.HEADER_SIZE_BYTES; ++i) {
+            AMQPHeader.validateByte(i, bytes[i]);
+        }
+    }
+
+    @Test
+    public void testValidateByteWithInvalidHeaderBytes() {
+        byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
+
+        for (int i = 0; i < AMQPHeader.HEADER_SIZE_BYTES; ++i) {
+            try {
+                AMQPHeader.validateByte(i, bytes[i]);
+                fail("Should throw IllegalArgumentException as bytes are invalid");
+            } catch (IllegalArgumentException iae) {
+                // Expected
+            }
+        }
+    }
+
+    @Test
+    public void testCreateWithNullBuffer() {
+        assertThrows(NullPointerException.class, () -> new AMQPHeader((ByteBuffer) null));
+    }
+
+    @Test
+    public void testCreateWithNullByte() {
+        assertThrows(NullPointerException.class, () -> new AMQPHeader((byte[]) null));
+    }
+
+    @Test
+    public void testCreateWithEmptyBuffer() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(ByteBuffer.allocate(128)));
+    }
+
+    @Test
+    public void testCreateWithOversizedBuffer() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderPrefix() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 0, 0, 1, 0, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderProtocol() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 4, 1, 0, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderMajor() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 2, 0, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderMinor() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 1, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderRevision() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 1}));
+    }
+
+    @Test
+    public void testValidateHeaderByte0WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(0, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte1WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(1, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte2WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(2, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte3WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(3, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte4WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(4, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte5WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(5, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte6WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(6, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte7WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(7, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByteIndexOutOfBounds() {
+        assertThrows(IndexOutOfBoundsException.class, () -> AMQPHeader.validateByte(8, (byte) 85));
+    }
+
+    @Test
+    public void testInvokeOnAMQPHeader() {
+        final AtomicBoolean amqpHeader = new AtomicBoolean();
+        final AtomicBoolean saslHeader = new AtomicBoolean();
+        final AtomicReference<String> captured = new AtomicReference<>();
+
+        AMQPHeader.getAMQPHeader().invoke(new HeaderHandler<String>() {
+
+            @Override
+            public void handleAMQPHeader(AMQPHeader header, String context) {
+                amqpHeader.set(true);
+                captured.set(context);
+            }
+
+            @Override
+            public void handleSASLHeader(AMQPHeader header, String context) {
+                saslHeader.set(true);
+                captured.set(context);
+            }
+        }, "test");
+
+        assertTrue(amqpHeader.get());
+        assertFalse(saslHeader.get());
+        assertEquals("test", captured.get());
+    }
+
+    @Test
+    public void testInvokeOnSASLHeader() {
+        final AtomicBoolean amqpHeader = new AtomicBoolean();
+        final AtomicBoolean saslHeader = new AtomicBoolean();
+        final AtomicReference<String> captured = new AtomicReference<>();
+
+        AMQPHeader.getSASLHeader().invoke(new HeaderHandler<String>() {
+
+            @Override
+            public void handleAMQPHeader(AMQPHeader header, String context) {
+                amqpHeader.set(true);
+                captured.set(context);
+            }
+
+            @Override
+            public void handleSASLHeader(AMQPHeader header, String context) {
+                saslHeader.set(true);
+                captured.set(context);
+            }
+        }, "test");
+
+        assertTrue(saslHeader.get());
+        assertFalse(amqpHeader.get());
+        assertEquals("test", captured.get());
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/FrameWriter.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/FrameWriter.java
new file mode 100644
index 0000000..cda0b72
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/FrameWriter.java
@@ -0,0 +1,143 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.test.driver.legacy;
+
+import java.nio.ByteBuffer;
+
+import org.apache.qpid.proton.codec.EncoderImpl;
+import org.apache.qpid.proton.codec.ReadableBuffer;
+import org.apache.qpid.proton.engine.impl.FrameWriterBuffer;
+
+/**
+ * Writes Frames to an internal buffer for later processing by the transport.
+ */
+class FrameWriter {
+
+    static final int DEFAULT_FRAME_BUFFER_FULL_MARK = 64 * 1024;
+    static final int FRAME_HEADER_SIZE = 8;
+
+    static final byte AMQP_FRAME_TYPE = 0;
+    static final byte SASL_FRAME_TYPE = 1;
+
+    private final EncoderImpl encoder;
+    private final FrameWriterBuffer frameBuffer = new FrameWriterBuffer();
+
+    // Configuration of this Frame Writer
+    private int maxFrameSize;
+    private final byte frameType;
+    private int frameBufferMaxBytes = DEFAULT_FRAME_BUFFER_FULL_MARK;
+
+    // State of current write operation, reset on start of each new write
+    private int frameStart;
+
+    // Frame Writer metrics
+    private long framesOutput;
+
+    FrameWriter(EncoderImpl encoder, int maxFrameSize, byte frameType) {
+        this.encoder = encoder;
+        this.maxFrameSize = maxFrameSize;
+        this.frameType = frameType;
+
+        encoder.setByteBuffer(frameBuffer);
+    }
+
+    boolean isFull() {
+        return frameBuffer.position() > frameBufferMaxBytes;
+    }
+
+    int readBytes(ByteBuffer dst) {
+        return frameBuffer.transferTo(dst);
+    }
+
+    long getFramesOutput() {
+        return framesOutput;
+    }
+
+    void setMaxFrameSize(int maxFrameSize) {
+        this.maxFrameSize = maxFrameSize;
+    }
+
+    void setFrameWriterMaxBytes(int maxBytes) {
+        this.frameBufferMaxBytes = maxBytes;
+    }
+
+    int getFrameWriterMaxBytes() {
+        return frameBufferMaxBytes;
+    }
+
+    void writeHeader(byte[] header) {
+        frameBuffer.put(header, 0, header.length);
+    }
+
+    void writeFrame(Object frameBody) {
+        writeFrame(0, frameBody, null, null);
+    }
+
+    void writeFrame(int channel, Object frameBody, ReadableBuffer payload, Runnable onPayloadTooLarge) {
+        frameStart = frameBuffer.position();
+
+        final int performativeSize = writePerformative(frameBody, payload, onPayloadTooLarge);
+        final int capacity = maxFrameSize > 0 ? maxFrameSize - performativeSize : Integer.MAX_VALUE;
+        final int payloadSize = Math.min(payload == null ? 0 : payload.remaining(), capacity);
+
+        if (payloadSize > 0) {
+            int oldLimit = payload.limit();
+            payload.limit(payload.position() + payloadSize);
+            frameBuffer.put(payload);
+            payload.limit(oldLimit);
+        }
+
+        endFrame(channel);
+
+        framesOutput++;
+    }
+
+    private int writePerformative(Object frameBody, ReadableBuffer payload, Runnable onPayloadTooLarge) {
+        frameBuffer.position(frameStart + FRAME_HEADER_SIZE);
+
+        if (frameBody != null) {
+            encoder.writeObject(frameBody);
+        }
+
+        int performativeSize = frameBuffer.position() - frameStart;
+
+        if (onPayloadTooLarge != null && maxFrameSize > 0 && payload != null && (payload.remaining() + performativeSize) > maxFrameSize) {
+            // Next iteration will re-encode the frame body again with updates from the <payload-to-large>
+            // handler and then we can move onto the body portion.
+            onPayloadTooLarge.run();
+            performativeSize = writePerformative(frameBody, payload, null);
+        }
+
+        return performativeSize;
+    }
+
+    private void endFrame(int channel) {
+        int frameSize = frameBuffer.position() - frameStart;
+        int originalPosition = frameBuffer.position();
+
+        frameBuffer.position(frameStart);
+        frameBuffer.putInt(frameSize);
+        frameBuffer.put((byte) 2);
+        frameBuffer.put(frameType);
+        frameBuffer.putShort((short) channel);
+        frameBuffer.position(originalPosition);
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/LegacyCodecOpenFramesTestDataGenerator.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/LegacyCodecOpenFramesTestDataGenerator.java
new file mode 100644
index 0000000..300412d
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/LegacyCodecOpenFramesTestDataGenerator.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.legacy;
+
+import org.apache.qpid.proton.amqp.UnsignedInteger;
+import org.apache.qpid.proton.amqp.transport.Open;
+
+/**
+ * Generates the test data used to create a tests for codec that read
+ * Frames encoded using the proton-j framework.
+ */
+public class LegacyCodecOpenFramesTestDataGenerator {
+
+    public static void main(String[] args) {
+        // 1: Empty Open - No fields set
+        Open emptyOpen = new Open();
+        emptyOpen.setContainerId("");
+        String emptyOpenFrameString = LegacyFrmaeDataGenerator.generateUnitTestVariable("emptyOpen", emptyOpen);
+        System.out.println(emptyOpenFrameString);
+
+        // 2: Basic Open - No capabilities or locals set
+        Open basicOpen = new Open();
+        basicOpen.setContainerId("container");
+        basicOpen.setHostname("localhost");
+        basicOpen.setMaxFrameSize(UnsignedInteger.valueOf(16384));
+        basicOpen.setIdleTimeOut(UnsignedInteger.valueOf(30000));
+        String basicOpenString = LegacyFrmaeDataGenerator.generateUnitTestVariable("basicOpen", basicOpen);
+        System.out.println(basicOpenString);
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/LegacyFrmaeDataGenerator.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/LegacyFrmaeDataGenerator.java
new file mode 100644
index 0000000..36d4dce
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/legacy/LegacyFrmaeDataGenerator.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.legacy;
+
+import java.nio.ByteBuffer;
+
+import org.apache.qpid.proton.codec.AMQPDefinedTypes;
+import org.apache.qpid.proton.codec.DecoderImpl;
+import org.apache.qpid.proton.codec.EncoderImpl;
+
+/**
+ * Generates test data that can be used to drive comparability tests
+ * between new and old codec and framing implementation.
+ */
+public class LegacyFrmaeDataGenerator {
+
+    private final static DecoderImpl decoder = new DecoderImpl();
+    private final static EncoderImpl encoder = new EncoderImpl(decoder);
+
+    private static final int DEFAULT_MAX_FRAME_SIZE = 32767;
+
+    private static final byte AMQP_FRAME = 0;
+
+    static {
+        AMQPDefinedTypes.registerAllTypes(decoder, encoder);
+    }
+
+    public static String generateUnitTestVariable(String varName, Object protonType) {
+        StringBuilder builder = new StringBuilder();
+
+        builder.append("    // ").append("Frame data for: ")
+               .append(protonType.getClass().getSimpleName()).append("\n");
+        builder.append("    // ").append("  ").append(protonType.toString()).append("\n");
+
+        // Create variable for test
+        builder.append("    final byte[] ")
+               .append(varName)
+               .append(" = new byte[] {")
+               .append(generateFrameEncoding(protonType))
+               .append("};");
+
+        return builder.toString();
+    }
+
+    public static String generateFrameEncoding(Object protonType) {
+        StringBuilder builder = new StringBuilder();
+        generateFrameEncodingFromProtonType(protonType, builder);
+        return builder.toString();
+    }
+
+    private static void generateFrameEncodingFromProtonType(Object instance, StringBuilder builder) {
+        FrameWriter writer = new FrameWriter(encoder, DEFAULT_MAX_FRAME_SIZE, AMQP_FRAME);
+        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_MAX_FRAME_SIZE);
+
+        writer.writeFrame(0, instance, null, null);
+        int frameSize = writer.readBytes(buffer);
+
+        for (int i = 0; i < frameSize; i++) {
+            builder.append(buffer.get(i));
+            if (i < frameSize - 1) {
+                builder.append(", ");
+            }
+        }
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/matches/types/EncodedCompositingDataSectionMatcherTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/matches/types/EncodedCompositingDataSectionMatcherTest.java
new file mode 100644
index 0000000..685e3db
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/matches/types/EncodedCompositingDataSectionMatcherTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matches.types;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.util.Random;
+
+import org.apache.qpid.proton.codec.EncodingCodes;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Data;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedCompositingDataSectionMatcher;
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+class EncodedCompositingDataSectionMatcherTest {
+
+    @Test
+    public void testIncomingReadWithoutDataSectionFailsValidation() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        ByteBuf incmoingBytes = Unpooled.buffer();
+
+        incmoingBytes.writeBytes(PAYLOAD);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertFalse(matcher.matches(incmoingBytes));
+    }
+
+    @Test
+    public void testValidatePartiallyTransmittedDataSectionShouldSucceed() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK = new byte[EXPECTED_SIZE / 2];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK);
+
+        ByteBuf partial1 = Unpooled.buffer();
+
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(EXPECTED_SIZE);
+        partial1.writeBytes(CHUNK);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+    }
+
+    @Test
+    public void testIncorrectPartiallyTransmittedDataSectionShouldNotSucceed() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK = new byte[EXPECTED_SIZE / 2];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED + 1);
+        bytesGenerator.nextBytes(CHUNK);
+
+        ByteBuf partial1 = Unpooled.buffer();
+
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(EXPECTED_SIZE);
+        partial1.writeBytes(CHUNK);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertFalse(matcher.matches(partial1));
+    }
+
+    @Test
+    public void testValidateSplitFameTransmittedDataSectionShouldSucceed() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK1 = new byte[EXPECTED_SIZE / 2];
+        final byte[] CHUNK2 = new byte[EXPECTED_SIZE / 2];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK1);
+        bytesGenerator.nextBytes(CHUNK2);
+
+        ByteBuf partial1 = Unpooled.buffer();
+        ByteBuf partial2 = Unpooled.buffer();
+
+        // First half arrives with preamble
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(EXPECTED_SIZE);
+        partial1.writeBytes(CHUNK1);
+
+        // Second half arrives without preamble as expected
+        partial2.writeBytes(CHUNK2);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+        assertThat(partial2, matcher);
+    }
+
+    @Test
+    public void testValidateMultiFameTransmittedDataSectionShouldSucceed() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK1 = new byte[EXPECTED_SIZE / 4];
+        final byte[] CHUNK2 = new byte[EXPECTED_SIZE / 4];
+        final byte[] CHUNK3 = new byte[EXPECTED_SIZE / 4];
+        final byte[] CHUNK4 = new byte[EXPECTED_SIZE / 4];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK1);
+        bytesGenerator.nextBytes(CHUNK2);
+        bytesGenerator.nextBytes(CHUNK3);
+        bytesGenerator.nextBytes(CHUNK4);
+
+        ByteBuf partial1 = Unpooled.buffer();
+        ByteBuf partial2 = Unpooled.buffer();
+        ByteBuf partial3 = Unpooled.buffer();
+        ByteBuf partial4 = Unpooled.buffer();
+
+        // First chunk arrives with preamble
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(EXPECTED_SIZE);
+        partial1.writeBytes(CHUNK1);
+
+        // Second chunk arrives without preamble as expected
+        partial2.writeBytes(CHUNK2);
+        // Third chunk arrives without preamble as expected
+        partial3.writeBytes(CHUNK3);
+        // Fourth chunk arrives without preamble as expected
+        partial4.writeBytes(CHUNK4);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+        assertThat(partial2, matcher);
+        assertThat(partial3, matcher);
+        assertThat(partial4, matcher);
+
+        // Anything else that arrives that is handed to this matcher should fail
+        assertFalse(matcher.matches(Unpooled.buffer().writeBytes(new byte[] { 3, 3, 3, 3})));
+    }
+
+    @Test
+    public void testUnevenTransferOfBytesSplitStillPassesValiation() {
+        final byte[] PAYLOAD = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        final byte[] CHUNK1 = new byte[] { 0, 1, 2 };
+        final byte[] CHUNK2 = new byte[] { 3, 4, 5, 6, 7, 8, 9 };
+
+        ByteBuf partial1 = Unpooled.buffer();
+        ByteBuf partial2 = Unpooled.buffer();
+
+        // First half arrives with preamble
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(PAYLOAD.length);
+        partial1.writeBytes(CHUNK1);
+
+        // Second half arrives without preamble as expected
+        partial2.writeBytes(CHUNK2);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+        assertThat(partial2, matcher);
+    }
+
+    @Test
+    public void testIncorrectSplitFameTransmittedDataSectionShouldNotSucceed() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK1 = new byte[EXPECTED_SIZE / 2];
+        final byte[] CHUNK2 = new byte[EXPECTED_SIZE / 2];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK1);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK2);
+
+        ByteBuf partial1 = Unpooled.buffer();
+        ByteBuf partial2 = Unpooled.buffer();
+
+        // First half arrives with preamble
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(EXPECTED_SIZE);
+        partial1.writeBytes(CHUNK1);
+
+        // Second half arrives without preamble as expected
+        partial2.writeBytes(CHUNK2);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+        assertFalse(matcher.matches(partial2));
+    }
+
+    @Test
+    public void testTrailingBytesCausesFailureInSameReadOperation() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK = new byte[EXPECTED_SIZE];
+        final byte[] EXTRA = new byte[] { 1, 0, 1, 0, 1, 0, 1 };
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK);
+
+        ByteBuf inboundBytes = Unpooled.buffer();
+
+        inboundBytes.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        inboundBytes.writeByte(EncodingCodes.SMALLULONG);
+        inboundBytes.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        inboundBytes.writeByte(EncodingCodes.VBIN32);
+        inboundBytes.writeInt(EXPECTED_SIZE);
+        inboundBytes.writeBytes(CHUNK);
+        inboundBytes.writeBytes(EXTRA);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertFalse(matcher.matches(inboundBytes));
+    }
+
+    @Test
+    public void testMultipleDataSectionsThatSupplyTheExpectedBytes() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK1 = new byte[EXPECTED_SIZE / 2];
+        final byte[] CHUNK2 = new byte[EXPECTED_SIZE / 2];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK1);
+        bytesGenerator.nextBytes(CHUNK2);
+
+        // First half arrives with preamble
+        ByteBuf partial1 = Unpooled.buffer();
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(CHUNK1.length);
+        partial1.writeBytes(CHUNK1);
+
+        // Second half arrives without preamble as expected
+        ByteBuf partial2 = Unpooled.buffer();
+        partial2.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial2.writeByte(EncodingCodes.SMALLULONG);
+        partial2.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial2.writeByte(EncodingCodes.VBIN32);
+        partial2.writeInt(CHUNK2.length);
+        partial2.writeBytes(CHUNK2);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+        assertThat(partial2, matcher);
+    }
+
+    @Test
+    public void testMultipleDataSectionsThatSupplyUnxpectedBytes() {
+        final long SEED = System.nanoTime();
+        final int EXPECTED_SIZE = 256;
+        final byte[] PAYLOAD = new byte[EXPECTED_SIZE];
+        final byte[] CHUNK1 = new byte[EXPECTED_SIZE / 2];
+        final byte[] CHUNK2 = new byte[EXPECTED_SIZE / 2];
+
+        Random bytesGenerator = new Random(SEED);
+        bytesGenerator.nextBytes(PAYLOAD);
+        bytesGenerator.setSeed(SEED);
+        bytesGenerator.nextBytes(CHUNK1);
+        bytesGenerator.nextBytes(CHUNK2);
+
+        // First half arrives with preamble
+        ByteBuf partial1 = Unpooled.buffer();
+        partial1.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial1.writeByte(EncodingCodes.SMALLULONG);
+        partial1.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial1.writeByte(EncodingCodes.VBIN32);
+        partial1.writeInt(CHUNK1.length);
+        partial1.writeBytes(CHUNK1);
+
+        // Second half arrives without preamble as expected
+        ByteBuf partial2 = Unpooled.buffer();
+        partial2.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        partial2.writeByte(EncodingCodes.SMALLULONG);
+        partial2.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        partial2.writeByte(EncodingCodes.VBIN32);
+        partial2.writeInt(CHUNK2.length + 1);
+        partial2.writeBytes(CHUNK2);
+        partial2.writeByte(64);
+
+        EncodedCompositingDataSectionMatcher matcher =
+            new EncodedCompositingDataSectionMatcher(PAYLOAD);
+
+        assertThat(partial1, matcher);
+        assertFalse(matcher.matches(partial2));
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/matches/types/EncodedPartialDataSectionMatcherTest.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/matches/types/EncodedPartialDataSectionMatcherTest.java
new file mode 100644
index 0000000..a1c195d
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/matches/types/EncodedPartialDataSectionMatcherTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.matches.types;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.apache.qpid.proton.codec.EncodingCodes;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.AmqpValue;
+import org.apache.qpid.protonj2.test.driver.codec.messaging.Data;
+import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedPartialDataSectionMatcher;
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+public class EncodedPartialDataSectionMatcherTest {
+
+    @Test
+    public void testValidatePartiallyTransmittedDataSection() {
+        final byte[] PAYLOAD = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final int EXPECTED_SIZE = 256;
+
+        ByteBuf body = Unpooled.buffer();
+
+        body.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        body.writeByte(EncodingCodes.SMALLULONG);
+        body.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        body.writeByte(EncodingCodes.VBIN32);
+        body.writeInt(EXPECTED_SIZE);
+        body.writeBytes(PAYLOAD);
+
+        EncodedPartialDataSectionMatcher matcher =
+            new EncodedPartialDataSectionMatcher(EXPECTED_SIZE, PAYLOAD);
+
+        assertThat(body, matcher);
+    }
+
+    @Test
+    public void testValidateImcorrectPartiallyTransmittedDataSectionInvalidSize() {
+        final byte[] PAYLOAD = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final int EXPECTED_SIZE = 256;
+
+        ByteBuf body = Unpooled.buffer();
+
+        body.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        body.writeByte(EncodingCodes.SMALLULONG);
+        body.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        body.writeByte(EncodingCodes.VBIN32);
+        body.writeInt(EXPECTED_SIZE + 1);
+        body.writeBytes(PAYLOAD);
+
+        EncodedPartialDataSectionMatcher matcher =
+            new EncodedPartialDataSectionMatcher(EXPECTED_SIZE, PAYLOAD);
+
+        assertFalse(matcher.matches(body));
+    }
+
+    @Test
+    public void testValidateImcorrectPartiallyTransmittedDataSectionInvalidDescribedType() {
+        final byte[] PAYLOAD = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final int EXPECTED_SIZE = 256;
+
+        ByteBuf body = Unpooled.buffer();
+
+        body.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        body.writeByte(EncodingCodes.SMALLULONG);
+        body.writeByte(AmqpValue.DESCRIPTOR_CODE.byteValue());
+        body.writeByte(EncodingCodes.VBIN32);
+        body.writeInt(EXPECTED_SIZE + 1);
+        body.writeBytes(PAYLOAD);
+
+        EncodedPartialDataSectionMatcher matcher =
+            new EncodedPartialDataSectionMatcher(EXPECTED_SIZE, PAYLOAD);
+
+        assertFalse(matcher.matches(body));
+    }
+
+    @Test
+    public void testValidateImcorrectPartiallyTransmittedDataSectionDescribedIsNotBinary() {
+        final byte[] PAYLOAD = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final int EXPECTED_SIZE = 256;
+
+        ByteBuf body = Unpooled.buffer();
+
+        body.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        body.writeByte(EncodingCodes.SMALLULONG);
+        body.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        body.writeByte(EncodingCodes.SYM8);
+        body.writeInt(EXPECTED_SIZE + 1);
+        body.writeBytes(PAYLOAD);
+
+        EncodedPartialDataSectionMatcher matcher =
+            new EncodedPartialDataSectionMatcher(EXPECTED_SIZE, PAYLOAD);
+
+        assertFalse(matcher.matches(body));
+    }
+
+    @Test
+    public void testValidateImcorrectPartiallyTransmittedDataSectionInvalidBody() {
+        final byte[] PAYLOAD = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final byte[] ACTUAL_PAYLOAD = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+        final int EXPECTED_SIZE = 256;
+
+        ByteBuf body = Unpooled.buffer();
+
+        body.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        body.writeByte(EncodingCodes.SMALLULONG);
+        body.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        body.writeByte(EncodingCodes.VBIN32);
+        body.writeInt(EXPECTED_SIZE + 1);
+        body.writeBytes(ACTUAL_PAYLOAD);
+
+        EncodedPartialDataSectionMatcher matcher =
+            new EncodedPartialDataSectionMatcher(EXPECTED_SIZE, PAYLOAD);
+
+        assertFalse(matcher.matches(body));
+    }
+}
diff --git a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/utils/TestPeerTestsBase.java b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/utils/TestPeerTestsBase.java
new file mode 100644
index 0000000..784545d
--- /dev/null
+++ b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/utils/TestPeerTestsBase.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.test.driver.utils;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class TestPeerTestsBase {
+
+    public static final boolean IS_WINDOWS = System.getProperty("os.name", "unknown").toLowerCase().contains("windows");
+
+    private final Logger LOG = LoggerFactory.getLogger(getClass());
+
+    private String testName;
+
+    public String getTestName() {
+        return testName;
+    }
+
+    @AfterEach
+    public void tearDown(TestInfo testInfo) throws Exception {
+        LOG.info("========== tearDown " + testInfo.getDisplayName() + " ==========");
+    }
+
+    @BeforeEach
+    public void setUp(TestInfo testInfo) throws Exception {
+        LOG.info("========== start " + testInfo.getDisplayName() + " ==========");
+        testName = testInfo.getDisplayName();
+    }
+}
diff --git a/protonj2-test-driver/src/test/resources/log4j.properties b/protonj2-test-driver/src/test/resources/log4j.properties
new file mode 100644
index 0000000..fb65374
--- /dev/null
+++ b/protonj2-test-driver/src/test/resources/log4j.properties
@@ -0,0 +1,33 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+log4j.rootLogger=TRACE, out, stdout
+
+# The logging properties used during tests, tune as needed.
+log4j.logger.org.apache.qpid.protonj2=TRACE
+
+# CONSOLE appender not used by default
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+
+# File appender
+log4j.appender.out=org.apache.log4j.FileAppender
+log4j.appender.out.layout=org.apache.log4j.PatternLayout
+log4j.appender.out.layout.ConversionPattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+log4j.appender.out.file=target/protonj2-test.log
+log4j.appender.out.append=true
diff --git a/protonj2-test-driver/src/test/resources/log4j2-test.properties b/protonj2-test-driver/src/test/resources/log4j2-test.properties
new file mode 100644
index 0000000..75e8ffc
--- /dev/null
+++ b/protonj2-test-driver/src/test/resources/log4j2-test.properties
@@ -0,0 +1,36 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name=ClientModuleTestPropertiesConfig
+status=warn
+
+appender.console.type=Console
+appender.console.name=STDOUT
+appender.console.layout.type=PatternLayout
+appender.console.layout.pattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+
+rootLogger.level=trace
+rootLogger.appenderRef.console.ref=STDOUT
+
+logger.proton.name=org.apache.qpid.protonj2
+logger.proton.level=trace
+
+logger.testpeer.name=org.apache.qpid.protonj2.test.driver
+logger.testpeer.level=trace
+
diff --git a/protonj2/pom.xml b/protonj2/pom.xml
new file mode 100644
index 0000000..1dd0c34
--- /dev/null
+++ b/protonj2/pom.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<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/maven-v4_0_0.xsd">
+  <parent>
+    <groupId>org.apache.qpid</groupId>
+    <artifactId>protonj2-parent</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>protonj2</artifactId>
+  <name>Qpid protonj2 AMQP protocol library</name>
+  <description>AMQP 1.0 protocol engine and codec library</description>
+  <packaging>bundle</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-buffer</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>proton-j</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>protonj2-test-driver</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-library</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>${project.basedir}/src/main/resources</directory>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>${project.basedir}/src/main/filtered-resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+    </resources>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <version>${maven.bundle.plugin.version}</version>
+        <configuration>
+          <instructions>
+            <Import-Package>
+              *
+            </Import-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Automatic-Module-Name>org.apache.qpid.protonj2</Automatic-Module-Name>
+            </manifestEntries>
+            <manifest>
+              <addDefaultSpecificationEntries>false</addDefaultSpecificationEntries>
+              <addDefaultImplementationEntries>false</addDefaultImplementationEntries>
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/protonj2/src/main/filtered-resources/org/apache/qpid/proton4j/engine/util/version.txt b/protonj2/src/main/filtered-resources/org/apache/qpid/proton4j/engine/util/version.txt
new file mode 100644
index 0000000..f2ab45c
--- /dev/null
+++ b/protonj2/src/main/filtered-resources/org/apache/qpid/proton4j/engine/util/version.txt
@@ -0,0 +1 @@
+${project.version}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonAbstractBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonAbstractBuffer.java
new file mode 100644
index 0000000..366ba9a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonAbstractBuffer.java
@@ -0,0 +1,736 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Base class used to hold the common implementation details for Proton buffer
+ * implementations.
+ */
+public abstract class ProtonAbstractBuffer implements ProtonBuffer {
+
+    protected int readIndex;
+    protected int writeIndex;
+    protected int markedReadIndex;
+    protected int markedWriteIndex;
+
+    private int maximumCapacity;
+
+    protected ProtonAbstractBuffer(int maximumCapacity) {
+        if (maximumCapacity < 0) {
+            throw new IllegalArgumentException("Maximum capacity should be non-negative but was: " + maximumCapacity);
+        }
+
+        this.maximumCapacity = maximumCapacity;
+    }
+
+    @Override
+    public int maxCapacity() {
+        return maximumCapacity;
+    }
+
+    @Override
+    public int getReadableBytes() {
+        return writeIndex - readIndex;
+    }
+
+    @Override
+    public int getWritableBytes() {
+        return capacity() - writeIndex;
+    }
+
+    @Override
+    public int getMaxWritableBytes() {
+        return maxCapacity() - writeIndex;
+    }
+
+    @Override
+    public int getReadIndex() {
+        return readIndex;
+    }
+
+    @Override
+    public ProtonBuffer setReadIndex(int value) {
+        if (value < 0 || value > writeIndex) {
+            throw new IndexOutOfBoundsException(String.format(
+                "readIndex: %d (expected: 0 <= readIndex <= writeIndex(%d))", value, writeIndex));
+        }
+        readIndex = value;
+        return this;
+    }
+
+    @Override
+    public int getWriteIndex() {
+        return writeIndex;
+    }
+
+    @Override
+    public ProtonBuffer setWriteIndex(int value) {
+        if (value < readIndex || value > capacity()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "writeIndex: %d (expected: readIndex(%d) <= writeIndex <= capacity(%d))",
+                value, readIndex, capacity()));
+        }
+        writeIndex = value;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setIndex(int readIndex, int writeIndex) {
+        if (readIndex < 0 || readIndex > writeIndex || writeIndex > capacity()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "readIndex: %d, writeIndex: %d (expected: 0 <= readeIndex <= writeIndex <= capacity(%d))",
+                readIndex, writeIndex, capacity()));
+        }
+        this.readIndex = readIndex;
+        this.writeIndex = writeIndex;
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer markReadIndex() {
+        this.markedReadIndex = readIndex;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer resetReadIndex() {
+        setReadIndex(markedReadIndex);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer markWriteIndex() {
+        this.markedWriteIndex = writeIndex;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer resetWriteIndex() {
+        setWriteIndex(markedWriteIndex);
+        return this;
+    }
+
+    @Override
+    public boolean isReadable() {
+        return writeIndex > readIndex;
+    }
+
+    @Override
+    public boolean isReadable(int numBytes) {
+        return writeIndex - readIndex >= numBytes;
+    }
+
+    @Override
+    public boolean isWritable() {
+        return capacity() > writeIndex;
+    }
+
+    @Override
+    public boolean isWritable(int numBytes) {
+        return capacity() - writeIndex >= numBytes;
+    }
+
+    @Override
+    public ProtonBuffer clear() {
+        readIndex = 0;
+        writeIndex = 0;
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer skipBytes(int length) {
+        checkReadableBytes(length);
+        readIndex += length;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer slice() {
+        return slice(readIndex, getReadableBytes());
+    }
+
+    @Override
+    public ProtonBuffer slice(int index, int length) {
+        checkIndex(index, length);
+        return new ProtonSlicedBuffer(this, index, length);
+    }
+
+    @Override
+    public ProtonBuffer duplicate() {
+        return new ProtonDuplicatedBuffer(this);
+    }
+
+    @Override
+    public ProtonBuffer copy() {
+        return copy(readIndex, getReadableBytes());
+    }
+
+    @Override
+    public abstract ProtonBuffer copy(int index, int length);
+
+    @Override
+    public ByteBuffer toByteBuffer() {
+        return toByteBuffer(readIndex, getReadableBytes());
+    }
+
+    @Override
+    public abstract ByteBuffer toByteBuffer(int index, int length);
+
+    @Override
+    public ProtonBuffer ensureWritable(int minWritableBytes) {
+        if (minWritableBytes < 0) {
+            throw new IllegalArgumentException(String.format(
+                "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
+        }
+
+        internalEnsureWritable(minWritableBytes);
+        return this;
+    }
+
+    //----- Read methods -----------------------------------------------------//
+
+    @Override
+    public byte readByte() {
+        internalCheckReadableBytes(1);
+        return getByte(readIndex++);
+    }
+
+    @Override
+    public boolean readBoolean() {
+        return readByte() != 0;
+    }
+
+    @Override
+    public short readShort() {
+        internalCheckReadableBytes(2);
+        short result = getShort(readIndex);
+        readIndex += 2;
+        return result;
+    }
+
+    @Override
+    public int readInt() {
+        internalCheckReadableBytes(4);
+        int result = getInt(readIndex);
+        readIndex += 4;
+        return result;
+    }
+
+    @Override
+    public long readLong() {
+        internalCheckReadableBytes(8);
+        long result = getLong(readIndex);
+        readIndex += 8;
+        return result;
+    }
+
+    @Override
+    public float readFloat() {
+        return Float.intBitsToFloat(readInt());
+    }
+
+    @Override
+    public double readDouble() {
+        return Double.longBitsToDouble(readLong());
+    }
+
+    @Override
+    public ProtonBuffer readBytes(byte[] target) {
+        readBytes(target, 0, target.length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(byte[] target, int length) {
+        readBytes(target, 0, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(byte[] target, int offset, int length) {
+        checkReadableBytes(length);
+        getBytes(readIndex, target, offset, length);
+        readIndex += length;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ProtonBuffer target) {
+        readBytes(target, target.getWritableBytes());
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ProtonBuffer target, int length) {
+        if (length > target.getWritableBytes()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "length(%d) exceeds target Writable Bytes:(%d), target is: %s", length, target.getWritableBytes(), target));
+        }
+        readBytes(target, target.getWriteIndex(), length);
+        target.setWriteIndex(target.getWriteIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ProtonBuffer target, int offset, int length) {
+        checkReadableBytes(length);
+        getBytes(readIndex, target, offset, length);
+        readIndex += length;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ByteBuffer dst) {
+        int length = dst.remaining();
+        checkReadableBytes(length);
+        getBytes(readIndex, dst);
+        readIndex += length;
+        return this;
+    }
+
+    //----- Write methods ----------------------------------------------------//
+
+    @Override
+    public ProtonBuffer writeByte(int value) {
+        internalEnsureWritable(1);
+        return setByte(writeIndex++, value);
+    }
+
+    @Override
+    public ProtonBuffer writeBoolean(boolean value) {
+        writeByte(value ? (byte) 1 : (byte) 0);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeShort(short value) {
+        internalEnsureWritable(2);
+        setShort(writeIndex, value);
+        writeIndex += 2;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeInt(int value) {
+        internalEnsureWritable(4);
+        setInt(writeIndex, value);
+        writeIndex += 4;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeLong(long value) {
+        internalEnsureWritable(8);
+        setLong(writeIndex, value);
+        writeIndex += 8;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeFloat(float value) {
+        writeInt(Float.floatToRawIntBits(value));
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeDouble(double value) {
+        writeLong(Double.doubleToRawLongBits(value));
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(byte[] source) {
+        writeBytes(source, 0, source.length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(byte[] source, int length) {
+        writeBytes(source, 0, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(byte[] source, int offset, int length) {
+        ensureWritable(length);
+        setBytes(writeIndex, source, offset, length);
+        writeIndex += length;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ProtonBuffer source) {
+        writeBytes(source, source.getReadableBytes());
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ProtonBuffer source, int length) {
+        if (length > source.getReadableBytes()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "length(%d) exceeds source Readable Bytes(%d), source is: %s", length, source.getReadableBytes(), source));
+        }
+        writeBytes(source, source.getReadIndex(), length);
+        source.setReadIndex(source.getReadIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ProtonBuffer source, int offset, int length) {
+        ensureWritable(length);
+        setBytes(writeIndex, source, offset, length);
+        writeIndex += length;
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ByteBuffer source) {
+        int length = source.remaining();
+        ensureWritable(length);
+        setBytes(writeIndex, source);
+        writeIndex += length;
+        return this;
+    }
+
+    //----- Get methods that call into other get methods ---------------------//
+
+    @Override
+    public boolean getBoolean(int index) {
+        return getByte(index) != 0;
+    }
+
+    @Override
+    public short getUnsignedByte(int index) {
+        return (short) (getByte(index) & 0xFF);
+    }
+
+    @Override
+    public int getUnsignedShort(int index) {
+        return getShort(index) & 0xFFFF;
+    }
+
+    @Override
+    public long getUnsignedInt(int index) {
+        return getInt(index) & 0xFFFFFFFFL;
+    }
+
+    @Override
+    public char getChar(int index) {
+        return (char) getShort(index);
+    }
+
+    @Override
+    public float getFloat(int index) {
+        return Float.intBitsToFloat(getInt(index));
+    }
+
+    @Override
+    public double getDouble(int index) {
+        return Double.longBitsToDouble(getLong(index));
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] target) {
+        getBytes(index, target, 0, target.length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer target) {
+        getBytes(index, target, target.getWritableBytes());
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer target, int length) {
+        getBytes(index, target, target.getWriteIndex(), length);
+        target.setWriteIndex(target.getWriteIndex() + length);
+        return this;
+    }
+
+    //----- Set methods that call into other set methods ---------------------//
+
+    @Override
+    public ProtonBuffer setBoolean(int index, boolean value) {
+        setByte(index, value ? 1 : 0);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setChar(int index, int value) {
+        setShort(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setFloat(int index, float value) {
+        setInt(index, Float.floatToRawIntBits(value));
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setDouble(int index, double value) {
+        setLong(index, Double.doubleToRawLongBits(value));
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] src) {
+        setBytes(index, src, 0, src.length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source) {
+        setBytes(index, source, source.getReadableBytes());
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int length) {
+        checkIndex(index, length);
+        if (source == null) {
+            throw new NullPointerException("src");
+        }
+        if (length > source.getReadableBytes()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "length(%d) exceeds source buffer Readable Bytes(%d), source is: %s", length, source.getReadableBytes(), source));
+        }
+
+        setBytes(index, source, source.getReadIndex(), length);
+        source.setReadIndex(source.getReadIndex() + length);
+        return this;
+    }
+
+    //----- Comparison and Equality implementations --------------------------//
+
+    @Override
+    public int hashCode() {
+        final int readable = getReadableBytes();
+        final int readableInts = readable >>> 2;
+        final int remainingBytes = readable & 3;
+
+        int hash = 1;
+        int position = getReadIndex();
+
+        for (int i = readableInts; i > 0; i --) {
+            hash = 31 * hash + getInt(position);
+            position += 4;
+        }
+
+        for (int i = remainingBytes; i > 0; i --) {
+            hash = 31 * hash + getByte(position++);
+        }
+
+        if (hash == 0) {
+            hash = 1;
+        }
+
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ProtonBuffer)) {
+            return false;
+        }
+
+        ProtonBuffer that = (ProtonBuffer) other;
+        if (this.getReadableBytes() != that.getReadableBytes()) {
+            return false;
+        }
+
+        final int readable = getReadableBytes();
+        final int longCount = readable >>> 3;
+        final int byteCount = readable & 7;
+
+        int positionSelf = getReadIndex();
+        int positionOther = that.getReadIndex();
+
+        for (int i = longCount; i > 0; i --) {
+            if (getLong(positionSelf) != that.getLong(positionOther)) {
+                return false;
+            }
+
+            positionSelf += 8;
+            positionOther += 8;
+        }
+
+        for (int i = byteCount; i > 0; i --) {
+            if (getByte(positionSelf) != that.getByte(positionOther)) {
+                return false;
+            }
+
+            positionSelf++;
+            positionOther++;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int compareTo(ProtonBuffer other) {
+        int length = getReadIndex() + Math.min(getReadableBytes(), other.getReadableBytes());
+
+        for (int i = this.getReadIndex(), j = getReadIndex(); i < length; i++, j++) {
+            int cmp = Integer.compare(getByte(i) & 0xFF, other.getByte(j) & 0xFF);
+            if (cmp != 0) {
+                return cmp;
+            }
+        }
+
+        return getReadableBytes() - other.getReadableBytes();
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() +
+               "{ read:" + getReadIndex() +
+               ", write: " + getWriteIndex() +
+               ", capacity: " + capacity() + "}";
+    }
+
+    @Override
+    public String toString(Charset charset) {
+        final String result;
+
+        if (hasArray()) {
+            result = new String(getArray(), getArrayOffset() + getReadIndex(), getReadableBytes(), charset);
+        } else {
+            byte[] copy = new byte[getReadableBytes()];
+            getBytes(getReadIndex(), copy);
+            result = new String(copy, 0, copy.length, charset);
+        }
+
+        return result;
+    }
+
+    //----- Validation methods for buffer access -----------------------------//
+
+    protected final void checkNewCapacity(int newCapacity) {
+        if (newCapacity < 0 || newCapacity > maxCapacity()) {
+            throw new IllegalArgumentException("newCapacity: " + newCapacity + " (expected: 0-" + maxCapacity() + ')');
+        }
+    }
+
+    public static boolean isOutOfBounds(int index, int length, int capacity) {
+        return (index | length | (index + length) | (capacity - (index + length))) < 0;
+    }
+
+    protected final void checkIndex(int index, int fieldLength) {
+        if (isOutOfBounds(index, fieldLength, capacity())) {
+            throw new IndexOutOfBoundsException(String.format(
+                "index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity()));
+        }
+    }
+
+    protected final void checkSourceIndex(int index, int length, int srcIndex, int srcCapacity) {
+        checkIndex(index, length);
+        if (isOutOfBounds(srcIndex, length, srcCapacity)) {
+            throw new IndexOutOfBoundsException(String.format(
+                "srcIndex: %d, length: %d (expected: range(0, %d))", srcIndex, length, srcCapacity));
+        }
+    }
+
+    protected final void checkDestinationIndex(int index, int length, int dstIndex, int dstCapacity) {
+        checkIndex(index, length);
+        if (isOutOfBounds(dstIndex, length, dstCapacity)) {
+            throw new IndexOutOfBoundsException(String.format(
+                "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dstCapacity));
+        }
+    }
+
+    protected final void checkReadableBytes(int minimumReadableBytes) {
+        if (minimumReadableBytes < 0) {
+            throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
+        }
+
+        internalCheckReadableBytes(minimumReadableBytes);
+    }
+
+    protected final void adjustIndexMarks(int decrement) {
+        final int markedReaderIndex = markedReadIndex;
+        if (markedReaderIndex <= decrement) {
+            markedReadIndex = 0;
+            final int markedWriterIndex = markedWriteIndex;
+            if (markedWriterIndex <= decrement) {
+                markedWriteIndex = 0;
+            } else {
+                markedWriteIndex = markedWriterIndex - decrement;
+            }
+        } else {
+            markedReadIndex = markedReaderIndex - decrement;
+            markedWriteIndex -= decrement;
+        }
+    }
+
+    private void internalCheckReadableBytes(int minimumReadableBytes) {
+        // Called when we know that we don't need to validate if the minimum readable
+        // value is negative.
+        if (readIndex > writeIndex - minimumReadableBytes) {
+            throw new IndexOutOfBoundsException(String.format(
+                "readIndex(%d) + length(%d) exceeds writeIndex(%d): %s",
+                readIndex, minimumReadableBytes, writeIndex, this));
+        }
+    }
+
+    private void internalEnsureWritable(int minWritableBytes) {
+        // Called when we know that we don't need to validate if the minimum writable
+        // value is negative.
+        if (minWritableBytes <= getWritableBytes()) {
+            return;
+        }
+
+        if (minWritableBytes > maxCapacity() - writeIndex) {
+            throw new IndexOutOfBoundsException(String.format(
+                "writeIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
+                writeIndex, minWritableBytes, maxCapacity(), this));
+        }
+
+        int newCapacity = calculateNewCapacity(writeIndex + minWritableBytes, maxCapacity());
+
+        // Adjust to the new capacity.
+        capacity(newCapacity);
+    }
+
+    private int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
+        if (minNewCapacity < 0) {
+            throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expectd: 0+)");
+        }
+
+        if (minNewCapacity > maxCapacity) {
+            throw new IllegalArgumentException(String.format(
+                "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
+                minNewCapacity, maxCapacity));
+        }
+
+        int newCapacity = 64;
+        while (newCapacity < minNewCapacity) {
+            newCapacity <<= 1;
+        }
+
+        return Math.min(newCapacity, maxCapacity);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBuffer.java
new file mode 100644
index 0000000..5b27eef
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBuffer.java
@@ -0,0 +1,1312 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Buffer type abstraction used to provide users of the proton library with
+ * a means of using their own type of byte buffer types in combination with the
+ * library tooling.
+ */
+public interface ProtonBuffer extends Comparable<ProtonBuffer> {
+
+    /**
+     * Return the underlying buffer object that backs this {@link ProtonBuffer} instance, or null
+     * if there is no backing object.
+     *
+     * This method should be overridden in buffer abstraction when access to the underlying backing
+     * store is needed such as when wrapping pooled resources that need explicit release calls.
+     *
+     * @return an underlying buffer object or other backing store for this buffer.
+     */
+    default Object unwrap() { return null; }
+
+    /**
+     * @return true if this buffer has a backing byte array that can be accessed.
+     */
+    boolean hasArray();
+
+    /**
+     * Returns the backing array for this ProtonBuffer instance if there is such an array or
+     * throws an exception if this ProtonBuffer implementation has no backing array.
+     * <p>
+     * Changes to the returned array are visible to other users of this ProtonBuffer.
+     *
+     * @return the backing byte array for this ProtonBuffer.
+     *
+     * @throws UnsupportedOperationException if this buffer type has no backing array.
+     */
+    byte[] getArray();
+
+    /**
+     * @return the offset of the first byte in the backing array belonging to this buffer.
+     *
+     * @throws UnsupportedOperationException if this buffer type has no backing array.
+     */
+    int getArrayOffset();
+
+    /**
+     * @return the number of bytes this buffer can currently contain.
+     */
+    int capacity();
+
+    /**
+     * Adjusts the capacity of this buffer.  If the new capacity is less than the current
+     * capacity, the content of this buffer is truncated.  If the new capacity is greater
+     * than the current capacity, the buffer is appended with unspecified data whose length is
+     * new capacity - current capacity.
+     *
+     * @param newCapacity
+     *      the new maximum capacity value of this buffer.
+     *
+     * @return this buffer for using in call chaining.
+     */
+    ProtonBuffer capacity(int newCapacity);
+
+    /**
+     * Returns the number of bytes that this buffer is allowed to grow to when write
+     * operations exceed the current capacity value.
+     *
+     * @return the number of bytes this buffer is allowed to grow to.
+     */
+    int maxCapacity();
+
+    /**
+     * Ensures that the requested number of bytes is available for write operations
+     * in the current buffer, growing the buffer if needed to meet the requested
+     * writable capacity.
+     *
+     * @param amount
+     *      The number of bytes beyond the current write index needed.
+     *
+     * @return this buffer for using in call chaining.
+     *
+     * @throws IllegalArgumentException if the amount given is less than zero.
+     * @throws IndexOutOfBoundsException if the amount given would result in the buffer
+     *         exceeding the maximum capacity for this buffer.
+     */
+    ProtonBuffer ensureWritable(int amount) throws IndexOutOfBoundsException, IllegalArgumentException;
+
+    /**
+     * Create a duplicate of this ProtonBuffer instance that shares the same backing
+     * data store and but maintains separate position index values.  Changes to one buffer
+     * are visible in any of its duplicates.  This method does not copy the read or write
+     * markers to the new buffer instance.
+     *
+     * @return a new ProtonBuffer instance that shares the backing data as this one.
+     */
+    ProtonBuffer duplicate();
+
+    /**
+     * Create a new ProtonBuffer whose contents are a subsequence of the contents of this
+     * {@link ProtonBuffer}.
+     * <p>
+     * The starting point of the new buffer starts at this buffer's current position, the
+     * marks and limits of the new buffer will be independent of this buffer however changes
+     * to the data backing the buffer will be visible in this buffer.
+     *
+     * @return a new {@link ProtonBuffer} whose contents are a subsequence of this buffer.
+     */
+    ProtonBuffer slice();
+
+    /**
+     * Create a new ProtonBuffer whose contents are a subsequence of the contents of this
+     * {@link ProtonBuffer}.
+     * <p>
+     * The starting point of the new buffer starts at given index into this buffer and spans
+     * the number of bytes given by the length.  Changes to the contents of this buffer or to
+     * the produced slice buffer are visible in the other.
+     *
+     * @param index
+     *      The index in this buffer where the slice should begin.
+     * @param length
+     *      The number of bytes to make visible to the new buffer from this one.
+     *
+     * @return a new {@link ProtonBuffer} whose contents are a subsequence of this buffer.
+     */
+    ProtonBuffer slice(int index, int length);
+
+    /**
+     * Create a deep copy of the readable bytes of this ProtonBuffer, the returned buffer can
+     * be modified without affecting the contents or position markers of this instance.
+     *
+     * @return a deep copy of this ProtonBuffer instance.
+     */
+    ProtonBuffer copy();
+
+    /**
+     * Returns a copy of this buffer's sub-region.  Modifying the content of
+     * the returned buffer or this buffer does not affect each other at all.
+     * This method does not modify the value returned from {@link #getReadIndex()}
+     * or {@link #getWriteIndex()} of this buffer.
+     *
+     * @param index
+     *      The index in this buffer where the copy should begin
+     * @param length
+     *      The number of bytes to copy to the new buffer from this one.
+     *
+     * @return a new ProtonBuffer instance containing the copied bytes.
+     */
+    ProtonBuffer copy(int index, int length);
+
+    /**
+     * Reset the position markers of this buffer, this method is not required to reset
+     * the data previously written to this buffer.
+     *
+     * @return this buffer for using in call chaining.
+     */
+    ProtonBuffer clear();
+
+    /**
+     * Returns a ByteBuffer that represents the readable bytes contained in this buffer.
+     * <p>
+     * This method should attempt to return a ByteBuffer that shares the backing data store
+     * with this buffer however if that is not possible it is permitted that the returned
+     * ByteBuffer contain a copy of the readable bytes of this ProtonBuffer.
+     *
+     * @return a ByteBuffer that represents the readable bytes of this buffer.
+     */
+    ByteBuffer toByteBuffer();
+
+    /**
+     * Returns a ByteBuffer that represents the given span of bytes from the readable portion
+     * of this buffer.
+     * <p>
+     * This method should attempt to return a ByteBuffer that shares the backing data store
+     * with this buffer however if that is not possible it is permitted that the returned
+     * ByteBuffer contain a copy of the readable bytes of this ProtonBuffer.
+     *
+     * @param index
+     *      The starting index in this where the ByteBuffer view should begin.
+     * @param length
+     *      The number of bytes to include in the ByteBuffer view.
+     *
+     * @return a ByteBuffer that represents the given view of this buffers readable bytes.
+     */
+    ByteBuffer toByteBuffer(int index, int length);
+
+    /**
+     * Returns a String created from the buffer's underlying bytes using the specified
+     * {@link java.nio.charset.Charset} for the newly created String.
+     *
+     * @param charset
+     *      the {@link java.nio.charset.Charset} to use to construct the new string.
+     *
+     * @return a string created from the buffer's underlying bytes using the given {@link java.nio.charset.Charset}.
+     */
+    String toString(Charset charset);
+
+    /**
+     * @return the number of bytes available for reading from this buffer.
+     */
+    int getReadableBytes();
+
+    /**
+     * @return the number of bytes that can be written to this buffer before the limit is hit.
+     */
+    int getWritableBytes();
+
+    /**
+     * Gets the current maximum number of bytes that can be written to this buffer.  This is
+     * the same value that can be computed by subtracting the current write index from the
+     * maximum buffer capacity.
+     *
+     * @return the maximum number of bytes that can be written to this buffer before the limit is hit.
+     */
+    int getMaxWritableBytes();
+
+    /**
+     * @return the current value of the read index for this buffer.
+     */
+    int getReadIndex();
+
+    /**
+     * Sets the read index for this buffer.
+     *
+     * @param value The index into the buffer where the read index should be positioned.
+     *
+     * @return this buffer for use in chaining.
+     *
+     * @throws IndexOutOfBoundsException if the value given is greater than the write index or negative.
+     */
+    ProtonBuffer setReadIndex(int value);
+
+    /**
+     * @return the current value of the write index for this buffer.
+     */
+    int getWriteIndex();
+
+    /**
+     * Sets the write index for this buffer.
+     *
+     * @param value The index into the buffer where the write index should be positioned.
+     *
+     * @return this buffer for use in chaining.
+     *
+     * @throws IndexOutOfBoundsException if the value less than the read index or greater than the capacity.
+     */
+    ProtonBuffer setWriteIndex(int value);
+
+    /**
+     * Used to set the read index and the write index in one call.  This methods allows for an update
+     * to the read index and write index to values that could not be set using simple setReadIndex and
+     * setWriteIndex call where the values would violate the constraints placed on them by the value
+     * of the other index.
+     *
+     * @param readIndex
+     *      The new read index to assign to this buffer.
+     * @param writeIndex
+     *      The new write index to assign to this buffer.
+     *
+     * @return this buffer for use in chaining.
+     *
+     * @throws IndexOutOfBoundsException if the values violate the basic tenants of readIndex and writeIndex
+     */
+    ProtonBuffer setIndex(int readIndex, int writeIndex);
+
+    /**
+     * Marks the current read index so that it can later be restored by a call to
+     * {@link ProtonBuffer#resetReadIndex}, the initial mark value is 0.
+     *
+     * @return this buffer for use in chaining.
+     */
+    ProtonBuffer markReadIndex();
+
+    /**
+     * Resets the current read index to the previously marked value.
+     *
+     * @return this buffer for use in chaining.
+     *
+     * @throws IndexOutOfBoundsException if the current write index is less than the marked read index.
+     */
+    ProtonBuffer resetReadIndex();
+
+    /**
+     * Marks the current write index so that it can later be restored by a call to
+     * {@link ProtonBuffer#resetWriteIndex}, the initial mark value is 0.
+     *
+     * @return this buffer for use in chaining.
+     */
+    ProtonBuffer markWriteIndex();
+
+    /**
+     * Resets the current write index to the previously marked value.
+     *
+     * @return this buffer for use in chaining.
+     *
+     * @throws IndexOutOfBoundsException if the current read index is greater than the marked write index.
+     */
+    ProtonBuffer resetWriteIndex();
+
+    /**
+     * @return true if the read index is less than the write index.
+     */
+    boolean isReadable();
+
+    /**
+     * Check if the given number of bytes can be read from the buffer.
+     *
+     * @param size
+     *      the size that is desired in readable bytes
+     *
+     * @return true if the buffer has at least the given number of readable bytes remaining.
+     */
+    boolean isReadable(int size);
+
+    /**
+     * Compares the remaining content of the current buffer with the remaining content of the
+     * given buffer, which must not be null. Each byte is compared in turn as an unsigned value,
+     * returning upon the first difference. If no difference is found before the end of one
+     * buffer, the shorter buffer is considered less than the other, or else if the same length
+     * then they are considered equal.
+     *
+     * @return  a negative, zero, or positive integer when this buffer is less than, equal to,
+     *          or greater than the given buffer.
+     * @see Comparable#compareTo(Object)
+     */
+    @Override int compareTo(ProtonBuffer buffer);
+
+    /**
+     * Gets a boolean from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    boolean getBoolean(int index);
+
+    /**
+     * Gets a byte from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    byte getByte(int index);
+
+    /**
+     * Gets a unsigned byte from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    short getUnsignedByte(int index);
+
+    /**
+     * Gets a 2-byte char from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    char getChar(int index);
+
+    /**
+     * Gets a short from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    short getShort(int index);
+
+    /**
+     * Gets a unsigned short from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    int getUnsignedShort(int index);
+
+    /**
+     * Gets a int from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    int getInt(int index);
+
+    /**
+     * Gets a unsigned int from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    long getUnsignedInt(int index);
+
+    /**
+     * Gets a long from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    long getLong(int index);
+
+    /**
+     * Gets a float from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    float getFloat(int index);
+
+    /**
+     * Gets a double from the specified index, this method will not modify the read or write
+     * index.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     *
+     * @return the value read from the given index.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or past the current buffer capacity.
+     */
+    double getDouble(int index);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the specified absolute {@code index} until the destination becomes
+     * non-writable.  This method is basically same with
+     * {@link #getBytes(int, ProtonBuffer, int, int)}, except that this
+     * method increases the {@code writeIndex} of the destination by the
+     * number of the transferred bytes while
+     * {@link #getBytes(int, ProtonBuffer, int, int)} does not.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * the source buffer (i.e. {@code this}).
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     * @param destination
+     *      the destination buffer for the bytes to be read
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0} or
+     *         if {@code index + dst.writableBytes} is greater than
+     *            {@code this.capacity}
+     */
+    ProtonBuffer getBytes(int index, ProtonBuffer destination);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the specified absolute {@code index}.  This method is basically same
+     * with {@link #getBytes(int, ProtonBuffer, int, int)}, except that this
+     * method increases the {@code writeIndex} of the destination by the
+     * number of the transferred bytes while
+     * {@link #getBytes(int, ProtonBuffer, int, int)} does not.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * the source buffer (i.e. {@code this}).
+     *
+     * @param index
+     *      the index in the buffer to start the read from
+     * @param destination
+     *      the destination buffer for the bytes to be read
+     * @param length
+     *      the number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0},
+     *         if {@code index + length} is greater than
+     *            {@code this.capacity}, or
+     *         if {@code length} is greater than {@code dst.writableBytes}
+     */
+    ProtonBuffer getBytes(int index, ProtonBuffer destination, int length);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the specified absolute {@code index}.
+     * This method does not modify {@code readIndex} or {@code writeIndex}
+     * of both the source (i.e. {@code this}) and the destination.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     * @param destination
+     *      The buffer where the bytes read will be written to
+     * @param destinationIndex
+     *      The index into the destination where the write starts
+     * @param length
+     *      The number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0},
+     *         if the specified {@code dstIndex} is less than {@code 0},
+     *         if {@code index + length} is greater than
+     *            {@code this.capacity}, or
+     *         if {@code dstIndex + length} is greater than
+     *            {@code dst.capacity}
+     */
+    ProtonBuffer getBytes(int index, ProtonBuffer destination, int destinationIndex, int length);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the specified absolute {@code index}.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * this buffer
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     * @param destination
+     *      The buffer where the bytes read will be written to
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0} or
+     *         if {@code index + dst.length} is greater than
+     *            {@code this.capacity}
+     */
+    ProtonBuffer getBytes(int index, byte[] destination);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the specified absolute {@code index}.
+     * This method does not modify {@code #getReadIndex()} or {@code #getWriteIndex()}
+     * of this buffer.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     * @param destination
+     *      The buffer where the bytes read will be written to
+     * @param offset
+     *      the offset into the destination to begin writing the bytes.
+     * @param length
+     *      the number of bytes to transfer from this buffer to the target buffer.
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0},
+     *         if the specified {@code offset} is less than {@code 0},
+     *         if {@code index + length} is greater than
+     *            {@code this.capacity}, or
+     *         if {@code offset + length} is greater than
+     *            {@code target.length}
+     */
+    ProtonBuffer getBytes(int index, byte[] destination, int offset, int length);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the specified absolute {@code index} until the destination's position
+     * reaches its limit.
+     * This method does not modify {@code #getReadIndex()} or {@code #getWriteIndex()} of
+     * this buffer while the destination's {@code position} will be increased.
+     *
+     * @param index
+     *      The index into the buffer where the value should be read.
+     * @param destination
+     *      The buffer where the bytes read will be written to
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0} or
+     *         if {@code index + destination.remaining()} is greater than
+     *            {@code #capacity()}
+     */
+    ProtonBuffer getBytes(int index, ByteBuffer destination);
+
+    /**
+     * Sets the byte value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setByte(int index, int value);
+
+    /**
+     * Sets the boolean value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setBoolean(int index, boolean value);
+
+    /**
+     * Sets the char value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setChar(int index, int value);
+
+    /**
+     * Sets the short value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setShort(int index, int value);
+
+    /**
+     * Sets the int value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setInt(int index, int value);
+
+    /**
+     * Sets the long value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setLong(int index, long value);
+
+    /**
+     * Sets the float value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setFloat(int index, float value);
+
+    /**
+     * Sets the double value at the given write index in this buffer's backing data store.
+     *
+     * @param index
+     *      The index to start the write from.
+     * @param value
+     *      The value to write at the given index.
+     *
+     * @return a reference to this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the index is negative or the write would exceed capcity.
+     */
+    ProtonBuffer setDouble(int index, double value);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the specified absolute {@code index} until the source buffer becomes
+     * unreadable.  This method is basically same with
+     * {@link #setBytes(int, ProtonBuffer, int, int)}, except that this
+     * method increases the {@code readIndex} of the source buffer by
+     * the number of the transferred bytes while
+     * {@link #setBytes(int, ProtonBuffer, int, int)} does not.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * the source buffer (i.e. {@code this}).
+     *
+     * @param index
+     *      The index in this buffer where the write operation starts.
+     * @param source
+     *      The source buffer from which the bytes are read.
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0} or
+     *         if {@code index + source.readableBytes} is greater than
+     *            {@code this.capacity}
+     */
+    ProtonBuffer setBytes(int index, ProtonBuffer source);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the specified absolute {@code index}.  This method is basically same
+     * with {@link #setBytes(int, ProtonBuffer, int, int)}, except that this
+     * method increases the {@code readIndex} of the source buffer by
+     * the number of the transferred bytes while
+     * {@link #setBytes(int, ProtonBuffer, int, int)} does not.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * the source buffer (i.e. {@code this}).
+     *
+     * @param index
+     *      The index in this buffer where the write operation starts.
+     * @param source
+     *      The source buffer from which the bytes are read.
+     * @param length
+     *      The number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0},
+     *         if {@code index + length} is greater than
+     *            {@code this.capacity}, or
+     *         if {@code length} is greater than {@code source.readableBytes}
+     */
+    ProtonBuffer setBytes(int index, ProtonBuffer source, int length);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the specified absolute {@code index}.
+     * This method does not modify {@code readIndex} or {@code writeIndex}
+     * of both the source (i.e. {@code this}) and the destination.
+     *
+     * @param index
+     *      The index in this buffer where the write operation starts.
+     * @param source
+     *      The source buffer from which the bytes are read.
+     * @param sourceIndex
+     *      The first index of the source
+     * @param length
+     *      The number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0},
+     *         if the specified {@code sourceIndex} is less than {@code 0},
+     *         if {@code index + length} is greater than
+     *            {@code this.capacity}, or
+     *         if {@code sourceIndex + length} is greater than
+     *            {@code source.capacity}
+     */
+    ProtonBuffer setBytes(int index, ProtonBuffer source, int sourceIndex, int length);
+
+    /**
+     * Transfers the specified source array's data to this buffer starting at
+     * the specified absolute {@code index}.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * this buffer.
+     *
+     * @param index
+     *      The index in this buffer where the write operation starts.
+     * @param source
+     *      The source buffer from which the bytes are read.
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0} or
+     *         if {@code index + source.length} is greater than
+     *            {@code this.capacity}
+     */
+    ProtonBuffer setBytes(int index, byte[] source);
+
+    /**
+     * Transfers the specified source array's data to this buffer starting at
+     * the specified absolute {@code index}.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * this buffer.
+     *
+     * @param index
+     *      The index in this buffer where the write operation starts.
+     * @param source
+     *      The source buffer from which the bytes are read.
+     * @param sourceIndex
+     *      The first index of the source
+     * @param length
+     *      The number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0},
+     *         if the specified {@code srcIndex} is less than {@code 0},
+     *         if {@code index + length} is greater than
+     *            {@code this.capacity}, or
+     *         if {@code srcIndex + length} is greater than {@code src.length}
+     */
+    ProtonBuffer setBytes(int index, byte[] source, int sourceIndex, int length);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the specified absolute {@code index} until the source buffer's position
+     * reaches its limit.
+     * This method does not modify {@code readIndex} or {@code writeIndex} of
+     * this buffer.
+     *
+     * @param index
+     *      The index in this buffer where the write operation starts.
+     * @param source
+     *      The source buffer from which the bytes are read.
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code index} is less than {@code 0} or
+     *         if {@code index + src.remaining()} is greater than
+     *            {@code this.capacity}
+     */
+    ProtonBuffer setBytes(int index, ByteBuffer source);
+
+    /**
+     * Increases the current {@code readIndex} of this buffer by the specified {@code length}.
+     *
+     * @param length
+     *      the number of bytes in this buffer to skip.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException
+     *         if {@code length} is greater than {@code this.readableBytes}
+     */
+    ProtonBuffer skipBytes(int length);
+
+    /**
+     * Reads one byte from the buffer and advances the read index by one.
+     *
+     * @return a single byte from the ProtonBuffer.
+     *
+     * @throws IndexOutOfBoundsException if there is no readable bytes left in the buffer.
+     */
+    byte readByte();
+
+    /**
+     * Reads bytes from this buffer and writes them into the destination byte array incrementing
+     * the read index by the value of the length of the destination array.
+     *
+     * @param target
+     *      The byte array to write into.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the target array is larger than the readable bytes.
+     */
+    ProtonBuffer readBytes(byte[] target);
+
+    /**
+     * Reads bytes from this buffer and writes them into the destination byte array incrementing
+     * the read index by the length value passed.
+     *
+     * @param target
+     *      The byte array to write into.
+     * @param length
+     *      The number of bytes to read into the given array.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the length is larger than the readable bytes, or length is
+     *         greater than the length of the target array, or length is negative.
+     */
+    ProtonBuffer readBytes(byte[] target, int length);
+
+    /**
+     * Reads bytes from this buffer and writes them into the destination byte array incrementing
+     * the read index by the length value passed, the bytes are read into the given buffer starting
+     * from the given offset value.
+     *
+     * @param target
+     *      The byte array to write into.
+     * @param offset
+     *      The offset into the given array where bytes are written.
+     * @param length
+     *      The number of bytes to read into the given array.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the offset is negative, or if the length is greater than
+     *         the current readable bytes or if the offset + length is great than the size of the target.
+     */
+    ProtonBuffer readBytes(byte[] target, int offset, int length);
+
+    /**
+     * Reads bytes from this buffer and writes them into the destination ProtonBuffer incrementing
+     * the read index by the value of the number of bytes written to the target.  The number of bytes
+     * written will be the equal to the writable bytes of the target buffer.  The write index of the
+     * target buffer must be incremented number of bytes written into it.
+     *
+     * @param target
+     *      The ProtonBuffer to write into.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IllegalArgumentException if the target buffer is this buffer.
+     * @throws IndexOutOfBoundsException if the target buffer has more writable bytes than this buffer
+     *         has readable bytes.
+     */
+    ProtonBuffer readBytes(ProtonBuffer target);
+
+    /**
+     * Reads bytes from this buffer and writes them into the destination ProtonBuffer incrementing
+     * the read index by the number of bytes written.  The write index of the target buffer must be
+     * incremented number of bytes written into it.
+     *
+     * @param target
+     *      The ProtonBuffer to write into.
+     * @param length
+     *      The number of bytes to read into the given buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the length value is greater than the readable bytes of
+     *         this buffer or is greater than the writable bytes of the target buffer..
+     */
+    ProtonBuffer readBytes(ProtonBuffer target, int length);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the current {@code readIndex} and increases the {@code readIndex}
+     * by the number of the transferred bytes (= {@code length}).
+     *
+     * @param target
+     *      The ProtonBuffer to write into.
+     * @param offset
+     *      The offset into the given buffer where bytes are written.
+     * @param length
+     *      The number of bytes to read into the given buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the offset is negative, or if the length is greater than
+     *         the current readable bytes or if the offset + length is great than the size of the target.
+     */
+    ProtonBuffer readBytes(ProtonBuffer target, int offset, int length);
+
+    /**
+     * Transfers this buffer's data to the specified destination starting at
+     * the current {@code readIndex} until the destination's position
+     * reaches its limit, and increases the {@code readIndex} by the
+     * number of the transferred bytes.
+     *
+     * @param destination
+     *      The target ByteBuffer to write into.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if the destination does not have enough capacity.
+     */
+    ProtonBuffer readBytes(ByteBuffer destination);
+
+    /**
+     * Reads a boolean value from the buffer and advances the read index by one.
+     *
+     * @return boolean value read from the buffer.
+     *
+     * @throws IndexOutOfBoundsException if a value cannot be read from the buffer.
+     */
+    boolean readBoolean();
+
+    /**
+     * Reads a short value from the buffer and advances the read index by two.
+     *
+     * @return short value read from the buffer.
+     *
+     * @throws IndexOutOfBoundsException if a value cannot be read from the buffer.
+     */
+    short readShort();
+
+    /**
+     * Reads a integer value from the buffer and advances the read index by four.
+     *
+     * @return integer value read from the buffer.
+     *
+     * @throws IndexOutOfBoundsException if a value cannot be read from the buffer.
+     */
+    int readInt();
+
+    /**
+     * Reads a long value from the buffer and advances the read index by eight.
+     *
+     * @return long value read from the buffer.
+     *
+     * @throws IndexOutOfBoundsException if a value cannot be read from the buffer.
+     */
+    long readLong();
+
+    /**
+     * Reads a float value from the buffer and advances the read index by four.
+     *
+     * @return float value read from the buffer.
+     *
+     * @throws IndexOutOfBoundsException if a value cannot be read from the buffer.
+     */
+    float readFloat();
+
+    /**
+     * Reads a double value from the buffer and advances the read index by eight.
+     *
+     * @return double value read from the buffer.
+     *
+     * @throws IndexOutOfBoundsException if a value cannot be read from the buffer.
+     */
+    double readDouble();
+
+    /**
+     * @return true if the buffer has bytes remaining between the write index and the capacity.
+     */
+    boolean isWritable();
+
+    /**
+     * Check if the requested number of bytes can be written into this buffer.
+     *
+     * @param size
+     *      The number of bytes that is being checked for writability.
+     *
+     * @return true if the buffer has space left for the given number of bytes to be written.
+     */
+    boolean isWritable(int size);
+
+    /**
+     * Writes a single byte to the buffer and advances the write index by one.
+     *
+     * @param value
+     *      The byte to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeByte(int value);
+
+    /**
+     * Writes the contents of the given byte array into the buffer and advances the write index by the
+     * length of the given array.
+     *
+     * @param value
+     *      The byte array to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeBytes(byte[] value);
+
+    /**
+     * Writes the contents of the given byte array into the buffer and advances the write index by the
+     * length value given.
+     *
+     * @param value
+     *      The byte array to write into the buffer.
+     * @param length
+     *      The number of bytes to write from the given array into this buffer
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeBytes(byte[] value, int length);
+
+    /**
+     * Writes the contents of the given byte array into the buffer and advances the write index by the
+     * length value given.  The bytes written into this buffer are read starting at the given offset
+     * into the passed in byte array.
+     *
+     * @param value
+     *      The byte array to write into the buffer.
+     * @param offset
+     *      The offset into the given array to start reading from.
+     * @param length
+     *      The number of bytes to write from the given array into this buffer
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeBytes(byte[] value, int offset, int length);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the current {@code writeIndex} until the source buffer becomes
+     * unreadable, and increases the {@code writeIndex} by the number of
+     * the transferred bytes.  This method is basically same with
+     * {@link #writeBytes(ProtonBuffer, int, int)}, except that this method
+     * increases the {@code readIndex} of the source buffer by the number of
+     * the transferred bytes while {@link #writeBytes(ProtonBuffer, int, int)}
+     * does not.
+     *
+     * @param source
+     *      The source buffer from which the bytes are read.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException
+     *         if {@code src.readableBytes} is greater than
+     *            {@code this.writableBytes}
+     */
+    ProtonBuffer writeBytes(ProtonBuffer source);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the current {@code writeIndex} and increases the {@code writeIndex}
+     * by the number of the transferred bytes (= {@code length}).  This method
+     * is basically same with {@link #writeBytes(ProtonBuffer, int, int)},
+     * except that this method increases the {@code readIndex} of the source
+     * buffer by the number of the transferred bytes (= {@code length}) while
+     * {@link #writeBytes(ProtonBuffer, int, int)} does not.
+     *
+     * @param source
+     *      The source buffer from which the bytes are read.
+     * @param length
+     *      The number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if {@code length} is greater than {@code this.writableBytes} or
+     *         if {@code length} is greater then {@code src.readableBytes}
+     */
+    ProtonBuffer writeBytes(ProtonBuffer source, int length);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the current {@code writeIndex} and increases the {@code writeIndex}
+     * by the number of the transferred bytes (= {@code length}).
+     *
+     * @param source
+     *      The source buffer from which the bytes are read.
+     * @param sourrceIndex
+     *      The first index of the source
+     * @param length
+     *      The number of bytes to transfer
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if the specified {@code srcIndex} is less than {@code 0},
+     *         if {@code srcIndex + length} is greater than
+     *            {@code src.capacity}, or
+     *         if {@code length} is greater than {@code this.writableBytes}
+     */
+    ProtonBuffer writeBytes(ProtonBuffer source, int sourrceIndex, int length);
+
+    /**
+     * Transfers the specified source buffer's data to this buffer starting at
+     * the current {@code writeIndex} until the source buffer's position
+     * reaches its limit, and increases the {@code writeIndex} by the
+     * number of the transferred bytes.
+     *
+     * @param source
+     *      The source buffer from which the bytes are read.
+     *
+     * @return this buffer for chaining
+     *
+     * @throws IndexOutOfBoundsException
+     *         if {@code src.remaining()} is greater than
+     *            {@code this.writableBytes}
+     */
+    ProtonBuffer writeBytes(ByteBuffer source);
+
+    /**
+     * Writes a single boolean to the buffer and advances the write index by one.
+     *
+     * @param value
+     *      The boolean to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeBoolean(boolean value);
+
+    /**
+     * Writes a single short to the buffer and advances the write index by two.
+     *
+     * @param value
+     *      The short to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeShort(short value);
+
+    /**
+     * Writes a single integer to the buffer and advances the write index by four.
+     *
+     * @param value
+     *      The integer to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeInt(int value);
+
+    /**
+     * Writes a single long to the buffer and advances the write index by eight.
+     *
+     * @param value
+     *      The long to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeLong(long value);
+
+    /**
+     * Writes a single float to the buffer and advances the write index by four.
+     *
+     * @param value
+     *      The float to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeFloat(float value);
+
+    /**
+     * Writes a single double to the buffer and advances the write index by eight.
+     *
+     * @param value
+     *      The double to write into the buffer.
+     *
+     * @return this ProtonBuffer for chaining.
+     *
+     * @throws IndexOutOfBoundsException if there is no room in the buffer for this write operation.
+     */
+    ProtonBuffer writeDouble(double value);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferAllocator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferAllocator.java
new file mode 100644
index 0000000..ae8d434
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferAllocator.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Interface for a ProtonBuffer allocator object that can be used by Proton
+ * objects to create memory buffers using the preferred type of the application
+ * or library that embeds the Proton engine.
+ */
+public interface ProtonBufferAllocator {
+
+    /**
+     * Create a new output ProtonBuffer instance with the given initial capacity and the
+     * maximum capacity should be that of the underlying buffer implementations limit.  The
+     * buffer implementation should support growing the buffer on an as needed basis to allow
+     * writes without the user needing to code extra capacity and buffer reallocation checks.
+     * <p>
+     * The returned buffer will be used for frame output from the Proton engine and
+     * can be a pooled buffer which the IO handler will then need to release once
+     * the buffer has been written.
+     *
+     * @param initialCapacity
+     *      The initial capacity to use when creating the new ProtonBuffer.
+     *
+     * @return a new ProtonBuffer instance with the given initial capacity.
+     */
+    ProtonBuffer outputBuffer(int initialCapacity);
+
+    /**
+     * Create a new output ProtonBuffer instance with the given initial capacity and the
+     * maximum capacity should that of the value specified by the caller.
+     * <p>
+     * The returned buffer will be used for frame output from the Proton engine and
+     * can be a pooled buffer which the IO handler will then need to release once
+     * the buffer has been written.
+     *
+     * @param initialCapacity
+     *      The initial capacity to use when creating the new ProtonBuffer.
+     * @param maximumCapacity
+     *      The largest amount of bytes the new ProtonBuffer is allowed to grow to.
+     *
+     * @return a new ProtonBuffer instance with the given initial capacity.
+     */
+    ProtonBuffer outputBuffer(int initialCapacity, int maximumCapacity);
+
+    /**
+     * Create a new ProtonBuffer instance with default initial capacity.  The buffer
+     * implementation should support growing the buffer on an as needed basis to allow
+     * writes without the user needing to code extra capacity and buffer reallocation
+     * checks.
+     *
+     * It is not recommended that these buffers be backed by a pooled resource as there
+     * is no defined release point within the buffer API and if used by an AMQP engine
+     * they could be lost as buffers are copied or aggregated together.
+     *
+     * @return a new ProtonBuffer instance with default initial capacity.
+     */
+    ProtonBuffer allocate();
+
+    /**
+     * Create a new ProtonBuffer instance with the given initial capacity and the
+     * maximum capacity should be that of the underlying buffer implementations
+     * limit.
+     *
+     * It is not recommended that these buffers be backed by a pooled resource as there
+     * is no defined release point within the buffer API and if used by an AMQP engine
+     * they could be lost as buffers are copied or aggregated together.
+     *
+     * @param initialCapacity
+     *      The initial capacity to use when creating the new ProtonBuffer.
+     *
+     * @return a new ProtonBuffer instance with the given initial capacity.
+     */
+    ProtonBuffer allocate(int initialCapacity);
+
+    /**
+     * Create a new ProtonBuffer instance with the given initial capacity and the
+     * maximum capacity should that of the value specified by the caller.
+     *
+     * It is not recommended that these buffers be backed by a pooled resource as there
+     * is no defined release point within the buffer API and if used by an AMQP engine
+     * they could be lost as buffers are copied or aggregated together.
+     *
+     * @param initialCapacity
+     *      The initial capacity to use when creating the new ProtonBuffer.
+     * @param maximumCapacity
+     *      The largest amount of bytes the new ProtonBuffer is allowed to grow to.
+     *
+     * @return a new ProtonBuffer instance with the given initial capacity.
+     */
+    ProtonBuffer allocate(int initialCapacity, int maximumCapacity);
+
+    /**
+     * Create a new ProtonBuffer that wraps the given byte array.
+     * <p>
+     * The capacity and maximum capacity for the resulting ProtonBuffer should equal
+     * to the length of the wrapped array and the returned array offset is zero.
+     *
+     * @param array
+     *      the byte array to wrap.
+     *
+     * @return a new ProtonBuffer that wraps the given array.
+     */
+    ProtonBuffer wrap(byte[] array);
+
+    /**
+     * Create a new ProtonBuffer that wraps the given byte array using the provided
+     * offset and length values to confine the view of that array.  The maximum capacity
+     * of the buffer should be that of the length of the wrapped array.
+     * <p>
+     * The capacity and maximum capacity for the resulting ProtonBuffer should equal
+     * to the length of the wrapped array and the returned array offset is zero.
+     *
+     * @param array
+     *      the byte array to wrap.
+     * @param offset
+     *      the offset into the array where the view begins.
+     * @param length
+     *      the number of bytes in the array to expose
+     *
+     * @return a new ProtonBuffer that wraps the given array.
+     */
+    ProtonBuffer wrap(byte[] array, int offset, int length);
+
+    /**
+     * Create a new ProtonBuffer that wraps the given ByteBuffer.  The maximum capacity
+     * of the returned buffer should be same as the remaining bytes within the wrapped
+     * {@link ByteBuffer}.
+     * <p>
+     * The capacity and maximum capacity of the returned ProtonBuffer will be the
+     * same as that of the underlying ByteBuffer.  The ProtonBuffer will return true
+     * from the {@link ProtonBuffer#hasArray()} method only when the wrapped ByteBuffer
+     * reports that it is backed by an array.
+     *
+     * @param buffer
+     *      the {@link ByteBuffer} to wrap.
+     *
+     * @return a new ProtonBuffer that wraps the given ByteBuffer.
+     */
+    ProtonBuffer wrap(ByteBuffer buffer);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferInputStream.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferInputStream.java
new file mode 100644
index 0000000..4e29d39
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferInputStream.java
@@ -0,0 +1,271 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+/**
+ * An InputStream that can be used to adapt a {@link ProtonBuffer} for use in the
+ * standard streams API.
+ */
+public class ProtonBufferInputStream extends InputStream implements DataInput {
+
+    private final ProtonBuffer buffer;
+    private final int initialReadIndex;
+
+    private boolean closed;
+
+    /**
+     * Creates a new {@link InputStream} instance that wraps the given {@link ProtonBuffer}
+     *
+     * @param buffer
+     */
+    public ProtonBufferInputStream(ProtonBuffer buffer) {
+        Objects.requireNonNull(buffer, "The given ProtonBuffer to wrap cannot be null");
+        this.buffer = buffer;
+        this.initialReadIndex = buffer.getReadIndex();
+    }
+
+    public int getBytesRead() {
+        return buffer.getReadIndex() - initialReadIndex;
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            super.close();
+        } finally {
+            this.closed = true;
+        }
+    }
+
+    @Override
+    public int available() throws IOException {
+        return buffer.getReadableBytes();
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        buffer.markReadIndex();
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        buffer.resetReadIndex();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return true;
+    }
+
+    @Override
+    public int read() throws IOException {
+        checkClosed();
+        if (buffer.getReadableBytes() == 0) {
+            return -1;
+        }
+
+        int result = buffer.readByte() & 0xff;
+
+        return result;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        checkClosed();
+
+        int available = available();
+        if (available == 0) {
+            return -1;
+        }
+
+        len = Math.min(available, len);
+        buffer.readBytes(b, off, len);
+        return len;
+    }
+
+    @Override
+    public long skip(long skipAmount) throws IOException {
+        checkClosed();
+        if (skipAmount > Integer.MAX_VALUE) {
+            return skipBytes(Integer.MAX_VALUE);
+        } else {
+            return skipBytes((int) skipAmount);
+        }
+    }
+
+    @Override
+    public int skipBytes(int skipAmount) throws IOException {
+        checkClosed();
+        int nBytes = Math.min(available(), skipAmount);
+        buffer.skipBytes(nBytes);
+        return nBytes;
+    }
+
+    @Override
+    public void readFully(byte[] target) throws IOException {
+        checkClosed();
+        checkAvailable(target.length);
+        buffer.readBytes(target);
+    }
+
+    @Override
+    public void readFully(byte[] target, int offset, int length) throws IOException {
+        checkClosed();
+        checkAvailable(length);
+        buffer.readBytes(target, offset, length);
+    }
+
+    @Override
+    public boolean readBoolean() throws IOException {
+        checkClosed();
+        checkAvailable(Byte.BYTES);
+        return buffer.readBoolean();
+    }
+
+    @Override
+    public byte readByte() throws IOException {
+        checkClosed();
+        checkAvailable(Byte.BYTES);
+        return buffer.readByte();
+    }
+
+    @Override
+    public int readUnsignedByte() throws IOException {
+        checkClosed();
+        checkAvailable(Byte.BYTES);
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public short readShort() throws IOException {
+        checkClosed();
+        checkAvailable(Short.BYTES);
+        return buffer.readShort();
+    }
+
+    @Override
+    public int readUnsignedShort() throws IOException {
+        checkClosed();
+        checkAvailable(Short.BYTES);
+        return buffer.readShort() & 0xFFFF;
+    }
+
+    @Override
+    public char readChar() throws IOException {
+        checkClosed();
+        checkAvailable(Short.BYTES);
+        return (char) buffer.readShort();
+    }
+
+    @Override
+    public int readInt() throws IOException {
+        checkClosed();
+        checkAvailable(Integer.BYTES);
+        return buffer.readInt();
+    }
+
+    @Override
+    public long readLong() throws IOException {
+        checkClosed();
+        checkAvailable(Long.BYTES);
+        return buffer.readLong();
+    }
+
+    @Override
+    public float readFloat() throws IOException {
+        checkClosed();
+        checkAvailable(Float.BYTES);
+        return buffer.readFloat();
+    }
+
+    @Override
+    public double readDouble() throws IOException {
+        checkClosed();
+        checkAvailable(Double.BYTES);
+        return buffer.readDouble();
+    }
+
+    private StringBuilder readBuffer;
+
+    @Override
+    public String readLine() throws IOException {
+        checkClosed();
+        int available = available();
+        if (available == 0) {
+            return null;
+        }
+
+        loop: do {
+            int c = buffer.readByte() & 0xff;
+            --available;
+            switch (c) {
+                case '\n':
+                    break loop;
+                case '\r':
+                    if (available > 0 && (char) buffer.getUnsignedByte(buffer.getReadIndex()) == '\n') {
+                        buffer.skipBytes(1);
+                        --available;
+                    }
+
+                    break loop;
+                default:
+                    if (readBuffer == null) {
+                        readBuffer = new StringBuilder();
+                    }
+                    readBuffer.append((char) c);
+            }
+        } while (available > 0);
+
+        final String result = readBuffer != null && readBuffer.length() > 0 ? readBuffer.toString() : "";
+
+        if (readBuffer != null) {
+            readBuffer.setLength(0);
+        }
+
+        return result;
+    }
+
+    @Override
+    public String readUTF() throws IOException {
+        checkClosed();
+        return DataInputStream.readUTF(this);
+    }
+
+    private void checkAvailable(int required) throws IOException {
+        if (required < 0) {
+            throw new IndexOutOfBoundsException("fieldSize cannot be a negative number");
+        }
+
+        if (required > available()) {
+            throw new EOFException("The required number of bytes is too high! Length is " + required +
+                                   ", but maximum readable is " + available());
+        }
+    }
+
+    private void checkClosed() throws IOException {
+        if (closed) {
+            throw new IOException("The ProtonBuffer InputStream has been closed");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferOutputStream.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferOutputStream.java
new file mode 100644
index 0000000..3d5a3f4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonBufferOutputStream.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * {@link ProtonBuffer} specialized {@link OutputStream} implementation which can be used to adapt
+ * the proton buffer types into code that uses the streams API.
+ */
+public class ProtonBufferOutputStream extends OutputStream implements DataOutput {
+
+    private final ProtonBuffer buffer;
+    private final int startWriteIndex;
+
+    private DataOutputStream cachedDataOut;
+    private boolean closed;
+
+    /**
+     * Create a new {@link OutputStream} which wraps the given buffer.
+     *
+     * @param buffer
+     *      The buffer that this stream will write to.
+     */
+    public ProtonBufferOutputStream(ProtonBuffer buffer) {
+        this.buffer = buffer;
+        this.startWriteIndex = buffer.getWriteIndex();
+    }
+
+    public int getBytesWritten() {
+        return buffer.getWriteIndex() - startWriteIndex;
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            super.close();
+        } finally {
+            this.closed = true;
+        }
+    }
+
+    @Override
+    public void writeBoolean(boolean value) throws IOException {
+        checkClosed();
+        buffer.writeBoolean(value);
+    }
+
+    @Override
+    public void write(int value) throws IOException {
+        checkClosed();
+        buffer.writeByte(value);
+    }
+
+    @Override
+    public void write(byte[] array, int offset, int length) throws IOException {
+        checkClosed();
+        if (length != 0) {
+            buffer.writeBytes(array, offset, length);
+        }
+    }
+
+    @Override
+    public void write(byte[] array) throws IOException {
+        checkClosed();
+        buffer.writeBytes(array);
+    }
+
+    @Override
+    public void writeByte(int value) throws IOException {
+        checkClosed();
+        buffer.writeByte(value);
+    }
+
+    @Override
+    public void writeShort(int value) throws IOException {
+        checkClosed();
+        buffer.writeShort((short) value);
+    }
+
+    @Override
+    public void writeChar(int value) throws IOException {
+        checkClosed();
+        buffer.writeShort((short) value);
+    }
+
+    @Override
+    public void writeInt(int value) throws IOException {
+        checkClosed();
+        buffer.writeInt(value);
+    }
+
+    @Override
+    public void writeLong(long value) throws IOException {
+        checkClosed();
+        buffer.writeLong(value);
+    }
+
+    @Override
+    public void writeFloat(float value) throws IOException {
+        checkClosed();
+        buffer.writeFloat(value);
+    }
+
+    @Override
+    public void writeDouble(double value) throws IOException {
+        checkClosed();
+        buffer.writeDouble(value);
+    }
+
+    @Override
+    public void writeBytes(String value) throws IOException {
+        checkClosed();
+        buffer.writeBytes(value.getBytes(StandardCharsets.US_ASCII));
+    }
+
+    @Override
+    public void writeChars(String value) throws IOException {
+        checkClosed();
+        for (int i = 0; i < value.length(); ++i) {
+            buffer.writeShort((short) value.charAt(i));
+        }
+    }
+
+    @Override
+    public void writeUTF(String value) throws IOException {
+        checkClosed();
+        if (cachedDataOut == null) {
+            cachedDataOut = new DataOutputStream(this);
+        }
+
+        cachedDataOut.writeUTF(value);
+    }
+
+    private void checkClosed() throws IOException {
+        if (closed) {
+            throw new IOException("The ProtonBuffer OutputStream has been closed");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteBuffer.java
new file mode 100644
index 0000000..d64d153
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteBuffer.java
@@ -0,0 +1,233 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Implementation of the ProtonBuffer interface that uses an array backing
+ * the buffer that is dynamically resized as bytes are written.
+ */
+public class ProtonByteBuffer extends ProtonAbstractBuffer {
+
+    public static final int DEFAULT_CAPACITY = 64;
+    public static final int DEFAULT_MAXIMUM_CAPACITY = Integer.MAX_VALUE;
+
+    private byte[] array;
+
+    public ProtonByteBuffer() {
+        this(DEFAULT_CAPACITY, DEFAULT_MAXIMUM_CAPACITY);
+    }
+
+    public ProtonByteBuffer(int initialCapacity) {
+        this(initialCapacity, DEFAULT_MAXIMUM_CAPACITY);
+    }
+
+    public ProtonByteBuffer(int initialCapacity, int maximumCapacity) {
+        super(maximumCapacity);
+
+        if (initialCapacity < 0) {
+            throw new IllegalArgumentException("Initial capacity cannot be < 0");
+        }
+
+        if (initialCapacity > maximumCapacity) {
+            throw new IllegalArgumentException("Initial capacity cannot exceed maximum capacity.");
+        }
+
+        this.array = new byte[initialCapacity];
+    }
+
+    public ProtonByteBuffer(byte[] array) {
+        this(array, DEFAULT_MAXIMUM_CAPACITY);
+    }
+
+    protected ProtonByteBuffer(byte[] array, int maximumCapacity) {
+        this(array, maximumCapacity, array.length);
+    }
+
+    protected ProtonByteBuffer(byte[] array, int maximumCapacity, int writeIndex) {
+        super(maximumCapacity);
+
+        if (array == null) {
+            throw new NullPointerException("Array to wrap cannot be null");
+        }
+
+        this.array = array;
+
+        setIndex(0, writeIndex);
+    }
+
+    @Override
+    public int capacity() {
+        return array.length;
+    }
+
+    @Override
+    public ProtonBuffer capacity(int newCapacity) {
+        checkNewCapacity(newCapacity);
+
+        int oldCapacity = array.length;
+        if (newCapacity > oldCapacity) {
+            byte[] newArray = new byte[newCapacity];
+            System.arraycopy(array, 0, newArray, 0, array.length);
+            array = newArray;
+        } else if (newCapacity < oldCapacity) {
+            byte[] newArray = new byte[newCapacity];
+            int readIndex = getReadIndex();
+            if (readIndex < newCapacity) {
+                int writeIndex = getWriteIndex();
+                if (writeIndex > newCapacity) {
+                    setWriteIndex(writeIndex = newCapacity);
+                }
+                System.arraycopy(array, readIndex, newArray, readIndex, writeIndex - readIndex);
+            } else {
+                setIndex(newCapacity, newCapacity);
+            }
+
+            array = newArray;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer copy(int index, int length) {
+        checkIndex(index, length);
+        byte[] copyOf = new byte[length];
+        System.arraycopy(array, index, copyOf, 0, length);
+        return new ProtonByteBuffer(copyOf, maxCapacity(), length);
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer(int index, int length) {
+        return ByteBuffer.wrap(array, index, length).slice();
+    }
+
+    @Override
+    public boolean hasArray() {
+        return true;
+    }
+
+    @Override
+    public byte[] getArray() {
+        return array;
+    }
+
+    @Override
+    public int getArrayOffset() {
+        return 0;
+    }
+
+    //----- Direct indexed get methods ---------------------------------------//
+
+    @Override
+    public byte getByte(int index) {
+        return ProtonByteUtils.readByte(array, index);
+    }
+
+    @Override
+    public short getShort(int index) {
+        return ProtonByteUtils.readShort(array, index);
+    }
+
+    @Override
+    public int getInt(int index) {
+        return ProtonByteUtils.readInt(array, index);
+    }
+
+    @Override
+    public long getLong(int index) {
+        return ProtonByteUtils.readLong(array, index);
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination, int destinationIndex, int length) {
+        checkDestinationIndex(index, length, destinationIndex, destination.capacity());
+
+        if (destination.hasArray()) {
+            System.arraycopy(array, index, destination.getArray(), destination.getArrayOffset() + destinationIndex, length);
+        } else {
+            destination.setBytes(destinationIndex, array, index, length);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] destination, int destinationIndex, int length) {
+        checkDestinationIndex(index, length, destinationIndex, destination.length);
+        System.arraycopy(array, index, destination, destinationIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ByteBuffer destination) {
+        checkIndex(index, destination.remaining());
+        destination.put(array, index, destination.remaining());
+        return this;
+    }
+
+    //----- Direct indexed set methods ---------------------------------------//
+
+    @Override
+    public ProtonBuffer setByte(int index, int value) {
+        ProtonByteUtils.writeByte((byte) value, array, index);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setShort(int index, int value) {
+        ProtonByteUtils.writeShort((short) value, array, index);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setInt(int index, int value) {
+        ProtonByteUtils.writeInt(value, array, index);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setLong(int index, long value) {
+        ProtonByteUtils.writeLong(value, array, index);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int sourceIndex, int length) {
+        checkSourceIndex(index, length, sourceIndex, source.capacity());
+        if (source.hasArray()) {
+            System.arraycopy(source.getArray(), source.getArrayOffset() + sourceIndex, array, index, length);
+        } else {
+            source.getBytes(sourceIndex, array, index, length);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] source, int sourceIndex, int length) {
+        checkSourceIndex(index, length, sourceIndex, source.length);
+        System.arraycopy(source, sourceIndex, array, index, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ByteBuffer src) {
+        src.get(array, index, src.remaining());
+        return this;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferAllocator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferAllocator.java
new file mode 100644
index 0000000..8cab968
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferAllocator.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Allocator for the default buffer type in Proton
+ */
+public final class ProtonByteBufferAllocator implements ProtonBufferAllocator {
+
+    public static final ProtonByteBufferAllocator DEFAULT = new ProtonByteBufferAllocator();
+
+    @Override
+    public ProtonByteBuffer allocate() {
+        return new ProtonByteBuffer();
+    }
+
+    @Override
+    public ProtonByteBuffer allocate(int initialCapacity) {
+        return new ProtonByteBuffer(initialCapacity);
+    }
+
+    @Override
+    public ProtonByteBuffer allocate(int initialCapacity, int maximumCapacity) {
+        return new ProtonByteBuffer(initialCapacity, maximumCapacity);
+    }
+
+    @Override
+    public ProtonBuffer outputBuffer(int initialCapacity) {
+        return allocate(initialCapacity);
+    }
+
+    @Override
+    public ProtonBuffer outputBuffer(int initialCapacity, int maximumCapacity) {
+        return allocate(initialCapacity, maximumCapacity);
+    }
+
+    @Override
+    public ProtonBuffer wrap(byte[] array) {
+        return new ProtonByteBuffer(array, array.length).slice();
+    }
+
+    @Override
+    public ProtonBuffer wrap(byte[] array, int offset, int length) {
+        return new ProtonByteBuffer(array, array.length).slice(offset, length);
+    }
+
+    @Override
+    public ProtonBuffer wrap(ByteBuffer buffer) {
+        if (buffer.isReadOnly() || buffer.isDirect()) {
+            throw new UnsupportedOperationException("Cannot wrap direct or read-only buffers");
+        }
+
+        if (buffer.hasArray()) {
+            return wrap(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+        } else {
+            throw new UnsupportedOperationException("Cannot wrap buffer that are not array backed");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteUtils.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteUtils.java
new file mode 100644
index 0000000..fdeeee5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonByteUtils.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+/**
+ * Set of Utility methods useful when dealing with byte arrays and other
+ * primitive types.
+ */
+public abstract class ProtonByteUtils {
+
+    public static byte[] toByteArray(byte value) {
+        return writeByte(value, new byte[Byte.BYTES], 0);
+    }
+
+    public static byte[] toByteArray(short value) {
+        return writeShort(value, new byte[Short.BYTES], 0);
+    }
+
+    public static byte[] toByteArray(int value) {
+        return writeInt(value, new byte[Integer.BYTES], 0);
+    }
+
+    public static byte[] toByteArray(long value) {
+        return writeLong(value, new byte[Long.BYTES], 0);
+    }
+
+    public static byte[] writeByte(byte value, byte[] destination, int offset) {
+        destination[offset] = value;
+
+        return destination;
+    }
+
+    public static byte[] writeShort(short value, byte[] destination, int offset) {
+        destination[offset++] = (byte) (value >>> 8);
+        destination[offset++] = (byte) (value >>> 0);
+
+        return destination;
+    }
+
+    public static byte[] writeInt(int value, byte[] destination, int offset) {
+        destination[offset++] = (byte) (value >>> 24);
+        destination[offset++] = (byte) (value >>> 16);
+        destination[offset++] = (byte) (value >>> 8);
+        destination[offset++] = (byte) (value >>> 0);
+
+        return destination;
+    }
+
+    public static byte[] writeLong(long value, byte[] destination, int offset) {
+        destination[offset++] = (byte) (value >>> 56);
+        destination[offset++] = (byte) (value >>> 48);
+        destination[offset++] = (byte) (value >>> 40);
+        destination[offset++] = (byte) (value >>> 32);
+        destination[offset++] = (byte) (value >>> 24);
+        destination[offset++] = (byte) (value >>> 16);
+        destination[offset++] = (byte) (value >>> 8);
+        destination[offset++] = (byte) (value >>> 0);
+
+        return destination;
+    }
+
+    public static byte readByte(byte[] array, int offset) {
+        return array[offset];
+    }
+
+    public static short readShort(byte[] array, int offset) {
+        return (short) ((array[offset++] & 0xFF) << 8 |
+                        (array[offset++] & 0xFF) << 0);
+    }
+
+    public static int readInt(byte[] array, int offset) {
+        return (array[offset++] & 0xFF) << 24 |
+               (array[offset++] & 0xFF) << 16 |
+               (array[offset++] & 0xFF) << 8 |
+               (array[offset++] & 0xFF) << 0;
+    }
+
+    public static long readLong(byte[] array, int offset) {
+        return (long) (array[offset++] & 0xFF) << 56 |
+               (long) (array[offset++] & 0xFF) << 48 |
+               (long) (array[offset++] & 0xFF) << 40 |
+               (long) (array[offset++] & 0xFF) << 32 |
+               (long) (array[offset++] & 0xFF) << 24 |
+               (long) (array[offset++] & 0xFF) << 16 |
+               (long) (array[offset++] & 0xFF) << 8 |
+               (long) (array[offset++] & 0xFF) << 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonCompositeBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonCompositeBuffer.java
new file mode 100644
index 0000000..eb1e0ec
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonCompositeBuffer.java
@@ -0,0 +1,771 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * A composite of 1 or more ProtonBuffer instances used when aggregating buffer views.
+ */
+public final class ProtonCompositeBuffer extends ProtonAbstractBuffer {
+
+    public static final int DEFAULT_MAXIMUM_CAPACITY = Integer.MAX_VALUE;
+
+    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+    private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.wrap(EMPTY_BYTE_ARRAY);
+
+    /**
+     * Aggregated count of all readable bytes in all buffers in the composite.
+     */
+    private int capacity;
+
+    /**
+     * Current number of ProtonBuffer chunks that are contained in this composite.
+     */
+    private int totalChunks;
+
+    /**
+     * The most recently used chunk which is used as a shortcut for linear read and write operations.
+     */
+    private Chunk lastAccessedChunk;
+
+    /**
+     * The fixed head pointer for the chain of buffer chunks
+     */
+    private final Chunk head;
+
+    /**
+     * The fixed tail pointer for the chain of buffer chunks
+     */
+    private final Chunk tail;
+
+    /**
+     * Creates a Composite Buffer instance with max capacity of {@link Integer#MAX_VALUE}.
+     */
+    public ProtonCompositeBuffer() {
+        this(Integer.MAX_VALUE);
+    }
+
+    /**
+     * Creates a Composite Buffer instance with the maximum capacity provided.
+     *
+     * @param maximumCapacity
+     *      The maximum capacity that this buffer can grow to.
+     */
+    public ProtonCompositeBuffer(int maximumCapacity) {
+        super(maximumCapacity);
+
+        this.head = new Chunk(null, 0, 0, -1, -1);
+        this.tail = new Chunk(null, 0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+        this.head.next = tail;
+        this.tail.prev = head;
+
+        // We never allow this to be null, it is either at the bounds or on a valid chunk.
+        this.lastAccessedChunk = head;
+    }
+
+    /**
+     * Appends the given byte array to the end of the buffer segments that comprise this composite
+     * {@link ProtonBuffer} instance.
+     *
+     * @param array
+     *      The array to append.
+     *
+     * @return this {@link ProtonCompositeBuffer} instance.
+     *
+     * @throws IndexOutOfBoundsException if the appended buffer would result in max capacity being exceeded.
+     */
+    public ProtonCompositeBuffer append(byte[] array) {
+        Objects.requireNonNull(array, "Cannot append null array to composite buffer.");
+        return append(ProtonByteBufferAllocator.DEFAULT.wrap(array));
+    }
+
+    /**
+     * Appends the given byte array to the end of the buffer segments that comprise this composite
+     * {@link ProtonBuffer} instance.
+     *
+     * @param array
+     *      The array to append.
+     * @param offset
+     *      The offset into the given array to index read and write operations.
+     * @param length
+     *      The usable portion of the given array.
+     *
+     * @return this {@link ProtonCompositeBuffer} instance.
+     *
+     * @throws IndexOutOfBoundsException if the appended buffer would result in max capacity being exceeded.
+     */
+    public ProtonCompositeBuffer append(byte[] array, int offset, int length) {
+        Objects.requireNonNull(array, "Cannot append null array to composite buffer.");
+        return append(ProtonByteBufferAllocator.DEFAULT.wrap(array, offset, length));
+    }
+
+    /**
+     * Appends the given {@link ProtonBuffer} to the end of the buffer segments that comprise this composite
+     * {@link ProtonBuffer} instance.
+     *
+     * @param buffer
+     *      The {@link ProtonBuffer} instance to append.
+     *
+     * @return this {@link ProtonCompositeBuffer} instance.
+     *
+     * @throws IndexOutOfBoundsException if the appended buffer would result in max capacity being exceeded.
+     */
+    public ProtonCompositeBuffer append(ProtonBuffer buffer) {
+        if (!buffer.isReadable()) {
+            return this;
+        }
+
+        if (buffer.getReadableBytes() + capacity() > maxCapacity()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "capacity(%d) + readableBytes(%d) exceeds maxCapacity(%d): %s",
+                capacity(), buffer.getReadableBytes(), maxCapacity(), this));
+        }
+
+        // If already at end we extend the write index to the new end of the composite
+        int newWriteIndex = writeIndex == capacity ? writeIndex + buffer.getReadableBytes() : writeIndex;
+        appendBuffer(buffer).setWriteIndex(newWriteIndex);
+
+        return this;
+    }
+
+    /**
+     * @return the total number of {@link ProtonBuffer} segments in this composite buffer isntance.
+     */
+    public int numberOfBuffers() {
+        return totalChunks;
+    }
+
+    /**
+     * For each of the buffers contained in this {@link ProtonCompositeBuffer} instance the
+     * given consumer will be invoked with a duplicate of the buffer that can be independently
+     * modified and not affect the contents of this buffer.
+     *
+     * @param consumer
+     *      The {@link Consumer} that will be called with each buffer instance.
+     *
+     * @return this {@link ProtonCompositeBuffer} instance.
+     */
+    public ProtonCompositeBuffer foreachBuffer(Consumer<ProtonBuffer> consumer) {
+        Chunk current = head.next;
+        while (current != tail) {
+            consumer.accept(current.buffer.duplicate());
+            current = current.next;
+        }
+
+        return this;
+    }
+
+    /**
+     * For each of the buffers contained in this {@link ProtonCompositeBuffer} instance the
+     * given consumer will be invoked with the {@link ProtonBuffer} that backs this composite
+     * instance.  Modifying the {@link ProtonBuffer} passed to the consumer modified the buffer
+     * backing this composite and as such leaves this composite in an unknown and invalid state.
+     *
+     * @param consumer
+     *      The {@link Consumer} that will be called with each buffer instance.
+     *
+     * @return this {@link ProtonCompositeBuffer} instance.
+     */
+    public ProtonCompositeBuffer foreachInternalBuffer(Consumer<ProtonBuffer> consumer) {
+        Chunk current = head.next;
+        while (current != tail) {
+            consumer.accept(current.buffer);
+            current = current.next;
+        }
+
+        return this;
+    }
+
+    /**
+     * For any buffer that preceeds the buffer pointed to by the current read index
+     * remove that buffer from to composite and discard.
+     *
+     * @return this {@link ProtonCompositeBuffer} instance.
+     */
+    public ProtonCompositeBuffer reclaimRead() {
+        final int readIndex = this.readIndex;
+        if (readIndex == 0) {
+            return this;
+        }
+
+        final int writeIndex = this.writeIndex;
+        if (readIndex == writeIndex && writeIndex == capacity()) {
+            capacity = 0;
+            totalChunks = 0;
+            lastAccessedChunk = head;
+            head.next = tail;
+            tail.prev = head;
+            setIndex(0, 0);
+            adjustIndexMarks(readIndex);
+        } else {
+            int removedSize = 0;
+
+            while (head.next != tail) {
+                if (head.next.endIndex >= readIndex) {
+                    break;
+                }
+
+                totalChunks--;
+                removedSize += head.next.length;
+
+                head.next = head.next.next;
+                head.next.prev = head;
+            }
+
+            if (removedSize == 0) {
+                return this;
+            }
+
+            if (lastAccessedChunk != null && lastAccessedChunk.endIndex < readIndex) {
+                lastAccessedChunk = head;
+            }
+
+            // All successive chunks need their index values reduced to reflect what was reclaimed.
+            Chunk current = head.next;
+            while (current != tail) {
+                current.startIndex -= removedSize;
+                current.endIndex -= removedSize;
+                current = current.next;
+            }
+
+            capacity -= removedSize;
+            setIndex(getReadIndex() - removedSize, getWriteIndex() - removedSize);
+            adjustIndexMarks(removedSize);
+        }
+
+        return this;
+    }
+
+    //----- ProtonAbstractBuffer API implementation
+
+    @Override
+    public boolean hasArray() {
+        switch (totalChunks) {
+            case 0:
+                return true;
+            case 1:
+                return head.next.buffer.hasArray();
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public byte[] getArray() {
+        switch (totalChunks) {
+            case 0:
+                return EMPTY_BYTE_ARRAY;
+            case 1:
+                return head.next.buffer.getArray();
+            default:
+                throw new UnsupportedOperationException("Buffer does not have a backing array.");
+        }
+    }
+
+    @Override
+    public int getArrayOffset() {
+        switch (totalChunks) {
+            case 0:
+                return 0;
+            case 1:
+                return head.next.buffer.getArrayOffset();
+            default:
+                throw new UnsupportedOperationException("Buffer does not have a backing array.");
+        }
+    }
+
+    @Override
+    public int capacity() {
+        return capacity;
+    }
+
+    @Override
+    public ProtonBuffer capacity(int newCapacity) {
+        checkNewCapacity(newCapacity);
+
+        if (newCapacity > capacity) {
+            final int amountNeeded = newCapacity - capacity;
+            appendBuffer(ProtonByteBufferAllocator.DEFAULT.allocate(amountNeeded, amountNeeded).setWriteIndex(amountNeeded));
+        } else if (newCapacity < capacity) {
+            int reductionTarget = capacity - newCapacity;
+            Chunk current = tail.prev;
+            while (current != head) {
+                if (current.length > reductionTarget) {
+                    ProtonBuffer sliced = current.buffer.slice(current.buffer.getReadIndex(), reductionTarget);
+                    Chunk replacement = new Chunk(
+                        sliced, 0, reductionTarget, current.startIndex, current.startIndex + reductionTarget);
+                    current.next.prev = replacement;
+                    current.prev.next = replacement;
+                    replacement.next = current.next;
+                    replacement.prev = current.prev;
+                    break;
+                } else {
+                    reductionTarget -= current.length;
+                    current.next.prev = current.prev;
+                    current.prev.next = current.next;
+                    totalChunks--;
+                }
+
+                current = current.prev;
+            }
+
+            capacity = newCapacity;
+            if (writeIndex > capacity) {
+                writeIndex = capacity;
+            }
+            if (readIndex > capacity) {
+                readIndex = capacity;
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer duplicate() {
+        return new ProtonDuplicatedBuffer(this);
+    }
+
+    @Override
+    public byte getByte(int index) {
+        checkIndex(index, Byte.BYTES);
+        Chunk targetChunk = findChunkWithIndex(index);
+        return targetChunk.readByte(index);
+    }
+
+    @Override
+    public short getShort(int index) {
+        checkIndex(index, Short.BYTES);
+
+        short result = 0;
+
+        lastAccessedChunk = findChunkWithIndex(index);
+
+        for (int i = Short.BYTES - 1; i >= 0; --i) {
+            result |= (lastAccessedChunk.readByte(index++) & 0xFF) << (i * Byte.SIZE);
+            if (lastAccessedChunk.endIndex < index) {
+                lastAccessedChunk = lastAccessedChunk.next;
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public int getInt(int index) {
+        checkIndex(index, Integer.BYTES);
+        lastAccessedChunk = findChunkWithIndex(index);
+
+        int result = 0;
+
+        for (int i = Integer.BYTES - 1; i >= 0; --i) {
+            result |= (lastAccessedChunk.readByte(index++) & 0xFF) << (i * Byte.SIZE);
+            if (lastAccessedChunk.endIndex < index) {
+                lastAccessedChunk = lastAccessedChunk.next;
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public long getLong(int index) {
+        checkIndex(index, Long.BYTES);
+        lastAccessedChunk = findChunkWithIndex(index);
+
+        long result = 0;
+
+        for (int i = Long.BYTES - 1; i >= 0; --i) {
+            result |= (long) (lastAccessedChunk.readByte(index++) & 0xFF) << (i * Byte.SIZE);
+            if (lastAccessedChunk.endIndex < index) {
+                lastAccessedChunk = lastAccessedChunk.next;
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination, int destinationIndex, int length) {
+        checkDestinationIndex(index, length, destinationIndex, destination.capacity());
+
+        while (length > 0) {
+            lastAccessedChunk = findChunkWithIndex(index);
+            final int readBytes = lastAccessedChunk.getBytes(index, destination, destinationIndex, length);
+            index += readBytes;
+            length -=readBytes;
+            destinationIndex += readBytes;
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] destination, int offset, int length) {
+        checkDestinationIndex(index, length, offset, destination.length);
+
+        while (length > 0) {
+            lastAccessedChunk = findChunkWithIndex(index);
+            final int readBytes = lastAccessedChunk.getBytes(index, destination, offset, length);
+            index += readBytes;
+            length -=readBytes;
+            offset += readBytes;
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ByteBuffer destination) {
+        checkIndex(index, destination.remaining());
+
+        while (destination.hasRemaining()) {
+            lastAccessedChunk = findChunkWithIndex(index);
+            final int readBytes = lastAccessedChunk.getBytes(index, destination);
+            index += readBytes;
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setByte(int index, int value) {
+        checkIndex(index, Byte.BYTES);
+        lastAccessedChunk = findChunkWithIndex(index);
+        lastAccessedChunk.writeByte(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setShort(int index, int value) {
+        checkIndex(index, Short.BYTES);
+        lastAccessedChunk = findChunkWithIndex(index);
+
+        lastAccessedChunk.writeByte(index++, (byte) (value >>> 8));
+        if (lastAccessedChunk.endIndex < index) {
+            lastAccessedChunk = lastAccessedChunk.next;
+        }
+        lastAccessedChunk.writeByte(index++, (byte) (value & 0xFF));
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setInt(int index, int value) {
+        checkIndex(index, Integer.BYTES);
+        lastAccessedChunk = findChunkWithIndex(index);
+
+        for (int i = Integer.BYTES - 1; i >= 0; --i) {
+            lastAccessedChunk.writeByte(index++, (byte) (value >>> (i * Byte.SIZE)));
+            if (lastAccessedChunk.endIndex < index) {
+                lastAccessedChunk = lastAccessedChunk.next;
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setLong(int index, long value) {
+        checkIndex(index, Long.BYTES);
+        lastAccessedChunk = findChunkWithIndex(index);
+
+        for (int i = Long.BYTES - 1; i >= 0; --i) {
+            lastAccessedChunk.writeByte(index++, (byte) (value >>> (i * Byte.SIZE)));
+            if (lastAccessedChunk.endIndex < index) {
+                lastAccessedChunk = lastAccessedChunk.next;
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int sourceIndex, int length) {
+        checkSourceIndex(index, length, sourceIndex, source.capacity());
+
+        while (length > 0) {
+            lastAccessedChunk = findChunkWithIndex(index);
+            final int writtenBytes = lastAccessedChunk.setBytes(index, source, sourceIndex, length);
+            index += writtenBytes;
+            length -= writtenBytes;
+            sourceIndex += writtenBytes;
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] source, int sourceIndex, int length) {
+        checkSourceIndex(index, length, sourceIndex, source.length);
+
+        while (length > 0) {
+            lastAccessedChunk = findChunkWithIndex(index);
+            final int writtenBytes = lastAccessedChunk.setBytes(index, source, sourceIndex, length);
+            index += writtenBytes;
+            length -= writtenBytes;
+            sourceIndex += writtenBytes;
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ByteBuffer source) {
+        checkSourceIndex(index, source.remaining() - source.position(), source.position(), source.remaining());
+
+        while (source.hasRemaining()) {
+            lastAccessedChunk = findChunkWithIndex(index);
+            final int writtenBytes = lastAccessedChunk.setBytes(index, source);
+            index += writtenBytes;
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer copy(int index, int length) {
+        checkIndex(index, length);
+
+        final ProtonBuffer copy = ProtonByteBufferAllocator.DEFAULT.allocate(length);
+        getBytes(index, copy);
+        copy.setWriteIndex(length);
+
+        return copy;
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer(int index, int length) {
+        switch (totalChunks) {
+            case 0:
+                return EMPTY_BYTE_BUFFER;
+            case 1:
+                return head.next.toByteBuffer(index, length);
+            default:
+                return internalToByteBuffer(index, length);
+        }
+    }
+
+    //----- Internal Support Framework API
+
+    private ByteBuffer internalToByteBuffer(int index, int length) {
+        checkIndex(index, length);
+
+        Chunk targetChunk = findChunkWithIndex(index);
+        if (targetChunk.isInRange(index, length)) {
+            return targetChunk.toByteBuffer(index, length);
+        } else {
+            byte[] copy = new byte[length];
+            int offset = 0;
+
+            while (length > 0) {
+                final int readBytes = targetChunk.getBytes(index, copy, offset, length);
+                index += readBytes;
+                length -=readBytes;
+                offset += readBytes;
+                targetChunk = findChunkWithIndex(index);
+            }
+
+            return ByteBuffer.wrap(copy);
+        }
+    }
+
+    private Chunk findChunkWithIndex(int index) {
+        if (index < lastAccessedChunk.startIndex) {
+            while (lastAccessedChunk.prev != head) {
+                lastAccessedChunk = lastAccessedChunk.prev;
+                if (lastAccessedChunk.isInRange(index)) {
+                    break;
+                }
+            }
+        } else if (index > lastAccessedChunk.endIndex) {
+            while (lastAccessedChunk.next != tail) {
+                lastAccessedChunk = lastAccessedChunk.next;
+                if (lastAccessedChunk.isInRange(index)) {
+                    break;
+                }
+            }
+        }
+
+        return lastAccessedChunk;
+    }
+
+    /*
+     * Appends the buffer to the end of the current set of chunks but does not alter the
+     * read or write index values, this is just a way to add capacity.
+     */
+    private ProtonCompositeBuffer appendBuffer(ProtonBuffer buffer) {
+        int window = buffer.getReadableBytes();
+        // We only read and write within the readable portion of the contained chunk so
+        // our capacity follows the total readable bytes from all chunks.
+        capacity += window;
+        totalChunks++;
+
+        final Chunk newChunk = new Chunk(buffer, buffer.getReadIndex(), window, tail.prev.endIndex + 1, tail.prev.endIndex + window);
+
+        // Link the new chunk onto the end updating any previous chunk as well.
+        newChunk.prev = tail.prev;
+        newChunk.next = tail;
+        tail.prev.next = newChunk;
+        tail.prev = newChunk;
+
+        if (lastAccessedChunk == head || lastAccessedChunk == tail) {
+            lastAccessedChunk = newChunk;
+        }
+
+        return this;
+    }
+
+    // TODO: Need to validate access of individual buffer chunks if API is added for that.
+    @SuppressWarnings("unused")
+    private void checkBufferIndex(int index) {
+        if (index < 0 || index > totalChunks) {
+            throw new IndexOutOfBoundsException(String.format(
+                    "The buffer index: %d (expected: >= 0 && <= numberOfBuffers(%d))",
+                    index, totalChunks));
+        }
+    }
+
+    /*
+     * A chunk of the composite buffer which holds the back buffer for that chunk and any
+     * additional data needed to represent this chunk in the chain.  Chucks are chained in
+     * order by link the first Chunk to the next using the next entry value.
+     */
+    private static class Chunk {
+
+        private final ProtonBuffer buffer;
+        private final int offset;
+        private final int length;
+
+        // We can more quickly traverse the chunks to locate an index read / write
+        // by tracking in this chunk where it lives in the buffer scope.
+        private int startIndex;
+        private int endIndex;
+
+        private Chunk next;
+        private Chunk prev;
+
+        public Chunk(ProtonBuffer buffer, int offset, int length, int startIndex, int endIndex) {
+            this.buffer = buffer;
+            this.offset = offset;
+            this.length = length;
+            this.startIndex = startIndex;
+            this.endIndex = endIndex;
+        }
+
+        public int getBytes(int index, ByteBuffer destination) {
+            final int readable = Math.min(length - (index - startIndex), destination.remaining());
+
+            int oldLimit = destination.limit();
+            destination.limit(destination.position() + readable);
+            try {
+                buffer.getBytes(offset(index), destination);
+            } finally {
+                destination.limit(oldLimit);
+            }
+
+            return readable;
+        }
+
+        public int setBytes(int index, ByteBuffer source) {
+            final int writeable = Math.min(length - (index - startIndex), source.remaining());
+
+            int oldLimit = source.limit();
+            source.limit(source.position() + writeable);
+            try {
+                buffer.setBytes(offset(index), source);
+            } finally {
+                source.limit(oldLimit);
+            }
+
+            return writeable;
+        }
+
+        public int getBytes(int index, byte[] destination, int offset, int desiredLength) {
+            final int readable = Math.min(length - (index - startIndex), desiredLength);
+
+            buffer.getBytes(offset(index), destination, offset, readable);
+
+            return readable;
+        }
+
+        public int setBytes(int index, byte[] source, int offset, int desiredLength) {
+            final int writeable = Math.min(length - (index - startIndex), desiredLength);
+
+            buffer.setBytes(offset(index), source, offset, writeable);
+
+            return writeable;
+        }
+
+        public int getBytes(int index, ProtonBuffer destination, int destinationIndex, int desiredLength) {
+            final int readable = Math.min(length - (index - startIndex), desiredLength);
+
+            buffer.getBytes(offset(index), destination, destinationIndex, readable);
+
+            return readable;
+        }
+
+        public int setBytes(int index, ProtonBuffer source, int sourceIndex, int desiredLength) {
+            final int writeable = Math.min(length - (index - startIndex), desiredLength);
+
+            buffer.setBytes(offset(index), source, sourceIndex, writeable);
+
+            return writeable;
+        }
+
+        public byte readByte(int index) {
+            return buffer.getByte(offset(index));
+        }
+
+        public void writeByte(int index, int value) {
+            buffer.setByte(offset(index), value);
+        }
+
+        public boolean isInRange(int index) {
+            if (index >= startIndex && index <= endIndex) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        public boolean isInRange(int index, int length) {
+            if (index >= startIndex && (index + (length - 1) <= endIndex)) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        public ByteBuffer toByteBuffer(int index, int length) {
+            return buffer.toByteBuffer(offset(index), length);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Chunk: { len=%d, sidx=%d, eidx=%d }", length, startIndex, endIndex);
+        }
+
+        private int offset(int index) {
+            return (index - startIndex) + offset;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonDuplicatedBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonDuplicatedBuffer.java
new file mode 100644
index 0000000..99a21c9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonDuplicatedBuffer.java
@@ -0,0 +1,185 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * A duplicated buffer wrapper for buffers known to be {@link ProtonAbstractBuffer} instances.
+ */
+public class ProtonDuplicatedBuffer extends ProtonAbstractBuffer {
+
+    private final ProtonAbstractBuffer buffer;
+
+    /**
+     * Wrap the given buffer to present a duplicate buffer with independent
+     * read and write index values.
+     *
+     * @param buffer
+     *      The {@link ProtonAbstractBuffer} instance to wrap with this instance.
+     */
+    public ProtonDuplicatedBuffer(ProtonAbstractBuffer buffer) {
+        super(buffer.maxCapacity());
+
+        Objects.requireNonNull(buffer, "The buffer being wrapped by a duplicate must not be null");
+
+        if (buffer instanceof ProtonDuplicatedBuffer) {
+            this.buffer = ((ProtonDuplicatedBuffer) buffer).buffer;
+        } else {
+            this.buffer = buffer;
+        }
+
+        setIndex(buffer.getReadIndex(), buffer.getWriteIndex());
+        markReadIndex();
+        markWriteIndex();
+    }
+
+    @Override
+    public boolean hasArray() {
+        return buffer.hasArray();
+    }
+
+    @Override
+    public byte[] getArray() {
+        return buffer.getArray();
+    }
+
+    @Override
+    public int getArrayOffset() {
+        return buffer.getArrayOffset();
+    }
+
+    @Override
+    public int capacity() {
+        return buffer.capacity();
+    }
+
+    @Override
+    public ProtonBuffer capacity(int newCapacity) {
+        buffer.capacity(newCapacity);
+        if (getReadIndex() < newCapacity) {
+            if (getWriteIndex() > newCapacity) {
+                setWriteIndex(newCapacity);
+            }
+        } else {
+            setIndex(newCapacity, newCapacity);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer duplicate() {
+        return new ProtonDuplicatedBuffer(this);
+    }
+
+    @Override
+    public ProtonBuffer slice(int index, int length) {
+        return buffer.slice(index, length);
+    }
+
+    @Override
+    public byte getByte(int index) {
+        return buffer.getByte(index);
+    }
+
+    @Override
+    public short getShort(int index) {
+        return buffer.getShort(index);
+    }
+
+    @Override
+    public int getInt(int index) {
+        return buffer.getInt(index);
+    }
+
+    @Override
+    public long getLong(int index) {
+        return buffer.getLong(index);
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination, int destinationIndex, int length) {
+        buffer.getBytes(index, destination, destinationIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] destination, int offset, int length) {
+        buffer.getBytes(index, destination, offset, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ByteBuffer destination) {
+        buffer.getBytes(index, destination);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setByte(int index, int value) {
+        buffer.setByte(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setShort(int index, int value) {
+        buffer.setShort(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setInt(int index, int value) {
+        buffer.setInt(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setLong(int index, long value) {
+        buffer.setLong(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int sourceIndex, int length) {
+        buffer.setBytes(index, source, sourceIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] source, int sourceIndex, int length) {
+        buffer.setBytes(index, source, sourceIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ByteBuffer source) {
+        buffer.setBytes(index, source);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer copy(int index, int length) {
+        return buffer.copy(index, length);
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer(int index, int length) {
+        return buffer.toByteBuffer(index, length);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBuffer.java
new file mode 100644
index 0000000..7657dea
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBuffer.java
@@ -0,0 +1,701 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Wrapper class for Netty ByteBuf instances
+ */
+public final class ProtonNettyByteBuffer implements ProtonBuffer {
+
+    private final ByteBuf wrapped;
+
+    public ProtonNettyByteBuffer(ByteBuf toWrap) {
+        this.wrapped = toWrap;
+    }
+
+    public ProtonNettyByteBuffer(int maximumCapacity) {
+        wrapped = Unpooled.buffer(1024, maximumCapacity);
+    }
+
+    @Override
+    public ByteBuf unwrap() {
+        return wrapped;
+    }
+
+    @Override
+    public int capacity() {
+        return wrapped.capacity();
+    }
+
+    @Override
+    public ProtonBuffer capacity(int newCapacity) {
+        wrapped.capacity(newCapacity);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer clear() {
+        wrapped.clear();
+        return this;
+    }
+
+    @Override
+    public int compareTo(ProtonBuffer other) {
+        int length = getReadIndex() + Math.min(getReadableBytes(), other.getReadableBytes());
+
+        for (int i = this.getReadIndex(), j = getReadIndex(); i < length; i++, j++) {
+            int cmp = Integer.compare(getByte(i) & 0xFF, other.getByte(j) & 0xFF);
+            if (cmp != 0) {
+                return cmp;
+            }
+        }
+
+        return getReadableBytes() - other.getReadableBytes();
+    }
+
+    @Override
+    public ProtonBuffer copy() {
+        return new ProtonNettyByteBuffer(wrapped.copy());
+    }
+
+    @Override
+    public ProtonBuffer copy(int index, int length) {
+        return new ProtonNettyByteBuffer(wrapped.copy(index, length));
+    }
+
+    @Override
+    public ProtonBuffer duplicate() {
+        return new ProtonNettyByteBuffer(wrapped.duplicate());
+    }
+
+    @Override
+    public ProtonBuffer ensureWritable(int minWritableBytes) throws IndexOutOfBoundsException, IllegalArgumentException {
+        wrapped.ensureWritable(minWritableBytes);
+        return this;
+    }
+
+    @Override
+    public byte[] getArray() {
+        return wrapped.array();
+    }
+
+    @Override
+    public int getArrayOffset() {
+        return wrapped.arrayOffset();
+    }
+
+    @Override
+    public boolean getBoolean(int index) {
+        return wrapped.getBoolean(index);
+    }
+
+    @Override
+    public byte getByte(int index) {
+        return wrapped.getByte(index);
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] destination) {
+        wrapped.getBytes(index, destination);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ByteBuffer destination) {
+        wrapped.getBytes(index, destination);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination) {
+        int length = destination.getWritableBytes();
+        getBytes(index, destination, destination.getWriteIndex(), length);
+        destination.setWriteIndex(destination.getWriteIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination, int length) {
+        getBytes(index, destination, destination.getWriteIndex(), length);
+        destination.setWriteIndex(destination.getWriteIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination, int offset, int length) {
+        if (destination.hasArray()) {
+            wrapped.getBytes(index, destination.getArray(), destination.getArrayOffset() + offset, length);
+        } else if (hasArray()) {
+            destination.setBytes(offset, getArray(), getArrayOffset() + index, length);
+        } else if (destination instanceof ProtonNettyByteBuffer) {
+            ProtonNettyByteBuffer wrapper = (ProtonNettyByteBuffer) destination;
+            wrapped.getBytes(index, wrapper.unwrap(), offset, length);
+        } else {
+            checkDestinationIndex(index, length, offset, destination.capacity());
+            for (int i = 0; i < length; ++i) {
+                destination.setByte(offset + i, wrapped.getByte(index + i));
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] destination, int offset, int length) {
+        wrapped.getBytes(index, destination, offset, length);
+        return this;
+    }
+
+    @Override
+    public char getChar(int index) {
+        return wrapped.getChar(index);
+    }
+
+    @Override
+    public double getDouble(int index) {
+        return wrapped.getDouble(index);
+    }
+
+    @Override
+    public float getFloat(int index) {
+        return wrapped.getFloat(index);
+    }
+
+    @Override
+    public int getInt(int index) {
+        return wrapped.getInt(index);
+    }
+
+    @Override
+    public long getLong(int index) {
+        return wrapped.getLong(index);
+    }
+
+    @Override
+    public int getReadIndex() {
+        return wrapped.readerIndex();
+    }
+
+    @Override
+    public int getReadableBytes() {
+        return wrapped.readableBytes();
+    }
+
+    @Override
+    public short getShort(int index) {
+        return wrapped.getShort(index);
+    }
+
+    @Override
+    public short getUnsignedByte(int index) {
+        return wrapped.getUnsignedByte(index);
+    }
+
+    @Override
+    public long getUnsignedInt(int index) {
+        return wrapped.getUnsignedInt(index);
+    }
+
+    @Override
+    public int getUnsignedShort(int index) {
+        return wrapped.getUnsignedShort(index);
+    }
+
+    @Override
+    public int getWritableBytes() {
+        return wrapped.writableBytes();
+    }
+
+    @Override
+    public int getMaxWritableBytes() {
+        return wrapped.maxWritableBytes();
+    }
+
+    @Override
+    public int getWriteIndex() {
+        return wrapped.writerIndex();
+    }
+
+    @Override
+    public boolean hasArray() {
+        return wrapped.hasArray();
+    }
+
+    @Override
+    public boolean isReadable() {
+        return wrapped.isReadable();
+    }
+
+    @Override
+    public boolean isReadable(int minReadableBytes) {
+        return wrapped.isReadable(minReadableBytes);
+    }
+
+    @Override
+    public boolean isWritable() {
+        return wrapped.isWritable();
+    }
+
+    @Override
+    public boolean isWritable(int minWritableBytes) {
+        return wrapped.isWritable(minWritableBytes);
+    }
+
+    @Override
+    public ProtonBuffer markReadIndex() {
+        wrapped.markReaderIndex();
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer markWriteIndex() {
+        wrapped.markWriterIndex();
+        return this;
+    }
+
+    @Override
+    public int maxCapacity() {
+        return wrapped.maxCapacity();
+    }
+
+    @Override
+    public boolean readBoolean() {
+        return wrapped.readBoolean();
+    }
+
+    @Override
+    public byte readByte() {
+        return wrapped.readByte();
+    }
+
+    @Override
+    public ProtonBuffer readBytes(byte[] destination) {
+        wrapped.readBytes(destination);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ByteBuffer destination) {
+        wrapped.readBytes(destination);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(byte[] destination, int length) {
+        wrapped.readBytes(destination, 0, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(byte[] destination, int offset, int length) {
+        wrapped.readBytes(destination, offset, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ProtonBuffer destination) {
+        readBytes(destination, destination.getWritableBytes());
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ProtonBuffer destination, int length) {
+        if (length > destination.getWritableBytes()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "length(%d) exceeds target Writable Bytes:(%d), target is: %s", length, destination.getWritableBytes(), destination));
+        }
+        readBytes(destination, destination.getWriteIndex(), length);
+        destination.setWriteIndex(destination.getWriteIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer readBytes(ProtonBuffer destination, int offset, int length) {
+        checkReadableBytes(length);
+        getBytes(wrapped.readerIndex(), destination, offset, length);
+        wrapped.skipBytes(length);
+        return this;
+    }
+
+    @Override
+    public double readDouble() {
+        return wrapped.readDouble();
+    }
+
+    @Override
+    public float readFloat() {
+        return wrapped.readFloat();
+    }
+
+    @Override
+    public int readInt() {
+        return wrapped.readInt();
+    }
+
+    @Override
+    public long readLong() {
+        return wrapped.readLong();
+    }
+
+    @Override
+    public short readShort() {
+        return wrapped.readShort();
+    }
+
+    @Override
+    public ProtonBuffer resetReadIndex() {
+        wrapped.resetReaderIndex();
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer resetWriteIndex() {
+        wrapped.resetWriterIndex();
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBoolean(int index, boolean value) {
+        wrapped.setBoolean(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setByte(int index, int value) {
+        wrapped.setByte(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] value) {
+        wrapped.setBytes(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ByteBuffer value) {
+        wrapped.setBytes(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer value) {
+        return setBytes(index, value, value.getReadableBytes());
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer value, int length) {
+        checkIndex(index, length);
+        if (value == null) {
+            throw new NullPointerException("src");
+        }
+        if (length > value.getReadableBytes()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "length(%d) exceeds source buffer Readable Bytes(%d), source is: %s", length, value.getReadableBytes(), value));
+        }
+
+        setBytes(index, value, value.getReadIndex(), length);
+        value.setReadIndex(value.getReadIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer value, int offset, int length) {
+        if (value instanceof ProtonNettyByteBuffer) {
+            wrapped.setBytes(index, (ByteBuf) value.unwrap(), offset, length);
+        } else if (value.hasArray()) {
+            wrapped.setBytes(index, value.getArray(), value.getArrayOffset() + offset, length);
+        } else if (hasArray()) {
+            value.getBytes(offset, getArray(), getArrayOffset() + index, length);
+        } else {
+            checkSourceIndex(index, length, offset, value.capacity());
+            for (int i = 0; i < length; ++i) {
+                wrapped.setByte(index + i, value.getByte(offset + i));
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] value, int offset, int length) {
+        wrapped.setBytes(index, value, offset, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setChar(int index, int value) {
+        wrapped.setChar(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setDouble(int index, double value) {
+        wrapped.setDouble(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setFloat(int index, float value) {
+        wrapped.setFloat(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setIndex(int readIndex, int writeIndex) {
+        wrapped.setIndex(readIndex, writeIndex);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setInt(int index, int value) {
+        wrapped.setInt(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setLong(int index, long value) {
+        wrapped.setLong(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setReadIndex(int index) {
+        wrapped.readerIndex(index);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setShort(int index, int value) {
+        wrapped.setShort(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setWriteIndex(int index) {
+        wrapped.writerIndex(index);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer skipBytes(int skippedBytes) {
+        wrapped.skipBytes(skippedBytes);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer slice() {
+        return new ProtonNettyByteBuffer(wrapped.slice());
+    }
+
+    @Override
+    public ProtonBuffer slice(int index, int length) {
+        return new ProtonNettyByteBuffer(wrapped.slice(index, length));
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer() {
+        return wrapped.nioBuffer();
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer(int index, int length) {
+        return wrapped.nioBuffer(index, length);
+    }
+
+    @Override
+    public String toString() {
+        return wrapped.toString();
+    }
+
+    @Override
+    public String toString(Charset charset) {
+        return wrapped.toString(charset);
+    }
+
+    @Override
+    public ProtonBuffer writeBoolean(boolean value) {
+        wrapped.writeBoolean(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeByte(int value) {
+        wrapped.writeByte(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ByteBuffer value) {
+        wrapped.writeBytes(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(byte[] value) {
+        wrapped.writeBytes(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(byte[] value, int length) {
+        wrapped.writeBytes(value, 0, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(byte[] array, int offset, int length) {
+        wrapped.writeBytes(array, offset, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ProtonBuffer value) {
+        return writeBytes(value, value.getReadableBytes());
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ProtonBuffer value, int length) {
+        if (length > value.getReadableBytes()) {
+            throw new IndexOutOfBoundsException(String.format(
+                "length(%d) exceeds source Readable Bytes(%d), source is: %s", length, value.getReadableBytes(), value));
+        }
+        writeBytes(value, value.getReadIndex(), length);
+        value.skipBytes(length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeBytes(ProtonBuffer value, int offset, int length) {
+        ensureWritable(length);
+        setBytes(wrapped.writerIndex(), value, offset, length);
+        wrapped.writerIndex(wrapped.writerIndex() + length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeDouble(double value) {
+        wrapped.writeDouble(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeFloat(float value) {
+        wrapped.writeFloat(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeInt(int value) {
+        wrapped.writeInt(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeLong(long value) {
+        wrapped.writeLong(value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer writeShort(short value) {
+        wrapped.writeShort(value);
+        return this;
+    }
+
+    @Override
+    public int hashCode() {
+        return wrapped.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ProtonBuffer)) {
+            return false;
+        }
+
+        ProtonBuffer that = (ProtonBuffer) other;
+        if (this.getReadableBytes() != that.getReadableBytes()) {
+            return false;
+        }
+
+        int index = getReadIndex();
+        for (int i = getReadableBytes() - 1, j = that.getReadableBytes() - 1; i >= index; i--, j--) {
+            if (!(getByte(i) == that.getByte(j))) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    //----- Internal Bounds Checking Utilities
+
+    protected final void checkReadableBytes(int minimumReadableBytes) {
+        if (minimumReadableBytes < 0) {
+            throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
+        }
+
+        internalCheckReadableBytes(minimumReadableBytes);
+    }
+
+    private void internalCheckReadableBytes(int minimumReadableBytes) {
+        // Called when we know that we don't need to validate if the minimum readable
+        // value is negative.
+        if (wrapped.readerIndex() > wrapped.writerIndex() - minimumReadableBytes) {
+            throw new IndexOutOfBoundsException(String.format(
+                "readIndex(%d) + length(%d) exceeds writeIndex(%d): %s",
+                wrapped.readerIndex(), minimumReadableBytes, wrapped.writerIndex(), this));
+        }
+    }
+
+    protected static boolean isOutOfBounds(int index, int length, int capacity) {
+        return (index | length | (index + length) | (capacity - (index + length))) < 0;
+    }
+
+    protected final void checkIndex(int index, int fieldLength) {
+        if (isOutOfBounds(index, fieldLength, capacity())) {
+            throw new IndexOutOfBoundsException(String.format(
+                "index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity()));
+        }
+    }
+
+    protected final void checkSourceIndex(int index, int length, int srcIndex, int srcCapacity) {
+        checkIndex(index, length);
+        if (isOutOfBounds(srcIndex, length, srcCapacity)) {
+            throw new IndexOutOfBoundsException(String.format(
+                "srcIndex: %d, length: %d (expected: range(0, %d))", srcIndex, length, srcCapacity));
+        }
+    }
+
+    protected final void checkDestinationIndex(int index, int length, int dstIndex, int dstCapacity) {
+        checkIndex(index, length);
+        if (isOutOfBounds(dstIndex, length, dstCapacity)) {
+            throw new IndexOutOfBoundsException(String.format(
+                "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dstCapacity));
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBufferAllocator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBufferAllocator.java
new file mode 100644
index 0000000..c4fe24e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBufferAllocator.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * A default {@link ProtonBufferAllocator} that creates wrapped Netty {@link ByteBuf} buffers.
+ *
+ * Output buffers are created using a Netty ByteBuf backed {@link ProtonBuffer} while the other
+ * methods may choose to use simple {@link ProtonByteBuffer} objects to reduce the number of
+ * intermediate allocations from wrapping one buffer type with another.
+ */
+public class ProtonNettyByteBufferAllocator implements ProtonBufferAllocator {
+
+    public static final ProtonNettyByteBufferAllocator DEFAULT = new ProtonNettyByteBufferAllocator();
+
+    @Override
+    public ProtonBuffer outputBuffer(int initialCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.buffer(initialCapacity));
+    }
+
+    @Override
+    public ProtonBuffer outputBuffer(int initialCapacity, int maximumCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.buffer(initialCapacity, maximumCapacity));
+    }
+
+    @Override
+    public ProtonByteBuffer allocate() {
+        return new ProtonByteBuffer();
+    }
+
+    @Override
+    public ProtonByteBuffer allocate(int initialCapacity) {
+        return new ProtonByteBuffer(initialCapacity);
+    }
+
+    @Override
+    public ProtonByteBuffer allocate(int initialCapacity, int maximumCapacity) {
+        return new ProtonByteBuffer(initialCapacity, maximumCapacity);
+    }
+
+    @Override
+    public ProtonBuffer wrap(byte[] array) {
+        return new ProtonByteBuffer(array, array.length).slice();
+    }
+
+    @Override
+    public ProtonBuffer wrap(byte[] array, int offset, int length) {
+        return new ProtonByteBuffer(array, array.length).slice(offset, length);
+    }
+
+    @Override
+    public ProtonBuffer wrap(ByteBuffer buffer) {
+        return new ProtonNettyByteBuffer(Unpooled.wrappedBuffer(buffer));
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNioByteBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNioByteBuffer.java
new file mode 100644
index 0000000..18e7807
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonNioByteBuffer.java
@@ -0,0 +1,248 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * ProtonBuffer wrapper around a NIO ByteBuffer instance.
+ */
+public class ProtonNioByteBuffer extends ProtonAbstractBuffer {
+
+    // TODO - Operations in this class assume the originating buffer is zero indexed, one alternative is to
+    //        slice but that might have unintended consequences.
+
+    private final ByteBuffer buffer;
+
+    public ProtonNioByteBuffer(ByteBuffer buffer) {
+        this(buffer, buffer.remaining());
+    }
+
+    public ProtonNioByteBuffer(ByteBuffer buffer, int writeIndex) {
+        super(buffer.remaining());
+
+        this.buffer = buffer.slice();
+
+        setIndex(0, writeIndex);
+    }
+
+    @Override
+    public ByteBuffer unwrap() {
+        return buffer;
+    }
+
+    @Override
+    public boolean hasArray() {
+        return buffer.hasArray();
+    }
+
+    @Override
+    public byte[] getArray() {
+        return buffer.array();
+    }
+
+    @Override
+    public int getArrayOffset() {
+        return buffer.arrayOffset();
+    }
+
+    @Override
+    public int capacity() {
+        return buffer.remaining();
+    }
+
+    @Override
+    public ProtonBuffer capacity(int newCapacity) {
+        if (newCapacity < 0) {
+            throw new IllegalArgumentException("Cannot alter a buffer's capacity to a negative value: " + newCapacity);
+        } else {
+            throw new UnsupportedOperationException("NIO Buffer wrapper cannot adjust capacity");
+        }
+    }
+
+    @Override
+    public byte getByte(int index) {
+        return buffer.get(index);
+    }
+
+    @Override
+    public short getShort(int index) {
+        return buffer.getShort(index);
+    }
+
+    @Override
+    public int getInt(int index) {
+        return buffer.getInt(index);
+    }
+
+    @Override
+    public long getLong(int index) {
+        return buffer.getLong(index);
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer destination, int destinationIndex, int length) {
+        checkDestinationIndex(index, length, destinationIndex, destination.capacity());
+        if (hasArray()) {
+            destination.setBytes(destinationIndex, getArray(), getArrayOffset() + index, length);
+        } else if (destination.hasArray()) {
+            int position = buffer.position();
+
+            buffer.position(index);
+            buffer.get(destination.getArray(), destination.getArrayOffset() + destinationIndex, length);
+            buffer.position(position);
+        } else {
+            while (length-- > 0) {
+                destination.setByte(destinationIndex++, buffer.get(index++));
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] destination, int offset, int length) {
+        checkDestinationIndex(index, length, offset, destination.length);
+        if (hasArray()) {
+            System.arraycopy(getArray(), getArrayOffset() + index, destination, offset, length);
+        } else {
+            final int position = buffer.position();
+
+            buffer.position(index);
+            buffer.get(destination, offset, length);
+            buffer.position(position);
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ByteBuffer destination) {
+        checkIndex(index, destination.remaining());
+        if (destination.hasArray()) {
+            final int position = buffer.position();
+
+            buffer.position(index);
+            buffer.get(destination.array(), destination.arrayOffset() + destination.position(), destination.remaining());
+            buffer.position(position);
+
+            destination.position(destination.limit());
+        } else if (hasArray()) {
+            destination.put(getArray(), getArrayOffset() + index, destination.remaining());
+        } else {
+            while (destination.hasRemaining()) {
+                destination.put(getByte(index++));
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setByte(int index, int value) {
+        buffer.put(index, (byte) value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setShort(int index, int value) {
+        buffer.putShort(index, (short) value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setInt(int index, int value) {
+        buffer.putInt(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setLong(int index, long value) {
+        buffer.putLong(index, value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int sourceIndex, int length) {
+        checkSourceIndex(index, length, sourceIndex, source.capacity());
+        if (source.hasArray()) {
+            final int position = buffer.position();
+
+            buffer.position(index);
+            buffer.put(source.getArray(), source.getArrayOffset() + sourceIndex, length);
+            buffer.position(position);
+        } else if (hasArray()) {
+            source.getBytes(sourceIndex, getArray(), getArrayOffset() + index, length);
+        } else {
+            while (length-- > 0) {
+                buffer.put(index++, source.getByte(sourceIndex++));
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] source, int sourceIndex, int length) {
+        checkSourceIndex(index, length, sourceIndex, source.length);
+
+        final int position = buffer.position();
+
+        buffer.position(index);
+        buffer.put(source, sourceIndex, length);
+        buffer.position(position);
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ByteBuffer source) {
+        checkSourceIndex(index, source.remaining(), source.position(), source.capacity());
+
+        final int position = buffer.position();
+
+        buffer.position(index);
+        buffer.put(source);
+        buffer.position(position);
+
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer copy(int index, int length) {
+        ProtonByteBuffer buffer = new ProtonByteBuffer(length);
+        getBytes(index, buffer, length);
+        return buffer;
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer(int index, int length) {
+        checkIndex(index, length);
+
+        int position = buffer.position();
+        int limit = buffer.limit();
+
+        buffer.position(index);
+        buffer.limit(index + length);
+
+        final ByteBuffer result = buffer.slice();
+
+        buffer.limit(limit);
+        buffer.position(position);
+
+        return result;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonSlicedBuffer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonSlicedBuffer.java
new file mode 100644
index 0000000..16061a6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/buffer/ProtonSlicedBuffer.java
@@ -0,0 +1,328 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Presents a sliced view of a {@link ProtonAbstractBuffer}.  The slice wraps the
+ * target buffer with a given offset into that buffer and a capped max capacity
+ * that limits how far into the wrapped buffer the slice will read or write.
+ *
+ * A sliced buffer does not allow capacity changes and as such any call to alter
+ * the capacity will result in an {@link UnsupportedOperationException}.
+ */
+public class ProtonSlicedBuffer extends ProtonAbstractBuffer {
+
+    private final ProtonAbstractBuffer buffer;
+    private final int indexOffset;
+
+    /**
+     * Creates a sliced view of the given {@link ProtonByteBuffer}.
+     *
+     * @param buffer
+     *      The buffer that this slice is a view of.
+     * @param offset
+     *      The offset into the buffer where this view starts.
+     * @param capacity
+     *      The amount of the buffer that this view spans.
+     */
+    protected ProtonSlicedBuffer(ProtonAbstractBuffer buffer, int offset, int capacity) {
+        super(capacity);
+
+        checkSliceOutOfBounds(offset, capacity, buffer);
+
+        if (buffer instanceof ProtonSlicedBuffer) {
+            this.buffer = ((ProtonSlicedBuffer) buffer).buffer;
+            this.indexOffset = ((ProtonSlicedBuffer) buffer).indexOffset + offset;
+        } else {
+            this.buffer = buffer;
+            this.indexOffset = offset;
+        }
+
+        setWriteIndex(capacity);
+    }
+
+    @Override
+    public boolean hasArray() {
+        return buffer.hasArray();
+    }
+
+    @Override
+    public byte[] getArray() {
+        return buffer.getArray();
+    }
+
+    @Override
+    public int getArrayOffset() {
+        return offset(buffer.getArrayOffset());
+    }
+
+    @Override
+    public int capacity() {
+        return maxCapacity();
+    }
+
+    @Override
+    public ProtonBuffer capacity(int newCapacity) {
+        throw new UnsupportedOperationException("Cannot adjust capacity of a buffer slice.");
+    }
+
+    @Override
+    public ProtonBuffer duplicate() {
+        return buffer.duplicate().setIndex(offset(getReadIndex()), offset(getWriteIndex()));
+    }
+
+    @Override
+    public ProtonBuffer slice(int index, int length) {
+        checkIndex(index, length);
+        return buffer.slice(offset(index), length);
+    }
+
+    @Override
+    public ProtonBuffer copy(int index, int length) {
+        checkIndex(index, length);
+        return buffer.copy(offset(index), length);
+    }
+
+    @Override
+    public ByteBuffer toByteBuffer(int index, int length) {
+        return buffer.toByteBuffer(offset(index), length).slice();
+    }
+
+    //----- Overridden absolute get methods ----------------------------------//
+
+    @Override
+    public boolean getBoolean(int index) {
+        checkIndex(index, 1);
+        return buffer.getBoolean(offset(index));
+    }
+
+    @Override
+    public byte getByte(int index) {
+        checkIndex(index, 1);
+        return buffer.getByte(offset(index));
+    }
+
+    @Override
+    public short getUnsignedByte(int index) {
+        checkIndex(index, 1);
+        return buffer.getUnsignedByte(offset(index));
+    }
+
+    @Override
+    public char getChar(int index) {
+        checkIndex(index, Character.BYTES);
+        return buffer.getChar(offset(index));
+    }
+
+    @Override
+    public short getShort(int index) {
+        checkIndex(index, Short.BYTES);
+        return buffer.getShort(offset(index));
+    }
+
+    @Override
+    public int getUnsignedShort(int index) {
+        checkIndex(index, Short.BYTES);
+        return buffer.getUnsignedShort(offset(index));
+    }
+
+    @Override
+    public int getInt(int index) {
+        checkIndex(index, Integer.BYTES);
+        return buffer.getInt(offset(index));
+    }
+
+    @Override
+    public long getUnsignedInt(int index) {
+        checkIndex(index, Integer.BYTES);
+        return buffer.getUnsignedInt(offset(index));
+    }
+
+    @Override
+    public long getLong(int index) {
+        checkIndex(index, Long.BYTES);
+        return buffer.getLong(offset(index));
+    }
+
+    @Override
+    public float getFloat(int index) {
+        checkIndex(index, Float.BYTES);
+        return buffer.getFloat(offset(index));
+    }
+
+    @Override
+    public double getDouble(int index) {
+        checkIndex(index, Double.BYTES);
+        return buffer.getDouble(offset(index));
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer dst) {
+        checkIndex(index, dst.getWritableBytes());
+        buffer.getBytes(offset(index), dst);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer dst, int length) {
+        checkIndex(index, length);
+        buffer.getBytes(offset(index), dst);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ProtonBuffer dst, int dstIndex, int length) {
+        checkIndex(index, length);
+        buffer.getBytes(offset(index), dst, dstIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] dst) {
+        checkIndex(index, dst.length);
+        buffer.getBytes(offset(index), dst);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, byte[] dst, int offset, int length) {
+        checkIndex(index, length);
+        buffer.getBytes(offset(index), dst, offset, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer getBytes(int index, ByteBuffer destination) {
+        checkIndex(index, destination.remaining());
+        buffer.getBytes(offset(index), destination);
+        return this;
+    }
+
+    //----- Overridden absolute set methods ----------------------------------//
+
+    @Override
+    public ProtonBuffer setByte(int index, int value) {
+        checkIndex(index, 1);
+        buffer.setByte(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBoolean(int index, boolean value) {
+        checkIndex(index, 1);
+        buffer.setBoolean(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setChar(int index, int value) {
+        checkIndex(index, Character.BYTES);
+        buffer.setChar(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setShort(int index, int value) {
+        checkIndex(index, Short.BYTES);
+        buffer.setShort(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setInt(int index, int value) {
+        checkIndex(index, Integer.BYTES);
+        buffer.setInt(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setLong(int index, long value) {
+        checkIndex(index, Long.BYTES);
+        buffer.setLong(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setFloat(int index, float value) {
+        checkIndex(index, Float.BYTES);
+        buffer.setFloat(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setDouble(int index, double value) {
+        checkIndex(index, Double.BYTES);
+        buffer.setDouble(offset(index), value);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source) {
+        checkIndex(index, source.getReadableBytes());
+        buffer.setBytes(offset(index), source);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int length) {
+        checkIndex(index, length);
+        buffer.setBytes(offset(index), source, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ProtonBuffer source, int sourceIndex, int length) {
+        checkIndex(index, length);
+        buffer.setBytes(offset(index), source, sourceIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] source) {
+        checkIndex(index, source.length);
+        buffer.setBytes(offset(index), source);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, byte[] src, int srcIndex, int length) {
+        checkIndex(index, length);
+        buffer.setBytes(offset(index), src, srcIndex, length);
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer setBytes(int index, ByteBuffer source) {
+        checkIndex(index, source.remaining());
+        buffer.setBytes(offset(index), source);
+        return this;
+    }
+
+    //----- Internal utility methods -----------------------------------------//
+
+    static void checkSliceOutOfBounds(int index, int length, ProtonAbstractBuffer buffer) {
+        if (isOutOfBounds(index, length, buffer.capacity())) {
+            throw new IndexOutOfBoundsException(buffer + ".slice(" + index + ", " + length + ')');
+        }
+    }
+
+    private int offset(int index) {
+        return index + indexOffset;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/CodecFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/CodecFactory.java
new file mode 100644
index 0000000..3dda045
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/CodecFactory.java
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+
+/**
+ * Factory Class used to create new instances of AMQP type
+ * Encoder and Decoder instances registered in the factory.
+ */
+public final class CodecFactory {
+
+    private static Encoder amqpTypeEncoder;
+    private static Encoder saslTypeEncoder;
+    private static Decoder amqpTypeDecoder;
+    private static Decoder saslTypeDecoder;
+
+    private CodecFactory() {
+    }
+
+    /**
+     * Sets an {@link Encoder} instance that will be returned from all calls to the
+     * {@link CodecFactory#getEncoder()}.  If no {@link Encoder} is configured then the
+     * calls to get an Encoder instance will return the default Encoder from the library.
+     *
+     * @param encoder
+     *      The encoder to return from all calls to the {@link CodecFactory#getEncoder()} method/
+     */
+    public static void setEncoder(Encoder encoder) {
+        amqpTypeEncoder = encoder;
+    }
+
+    /**
+     * Sets an {@link Decoder} instance that will be returned from all calls to the
+     * {@link CodecFactory#getDecoder()}.  If no {@link Decoder} is configured then the
+     * calls to get an Decoder instance will return the default Decoder from the library.
+     *
+     * @param decoder
+     *      The decoder to return from all calls to the {@link CodecFactory#getDecoder()} method/
+     */
+    public static void setDecoder(Decoder decoder) {
+        amqpTypeDecoder = decoder;
+    }
+
+    /**
+     * Sets an {@link Encoder} instance that will be returned from all calls to the
+     * {@link CodecFactory#getSaslEncoder()}.  If no {@link Encoder} is configured then the
+     * calls to get an Encoder instance will return the default Encoder from the library.
+     * The Encoder configured should only accept encodes of the SASL AMQP types.
+     *
+     * @param encoder
+     *      The encoder to return from all calls to the {@link CodecFactory#getSaslEncoder()} method/
+     */
+    public static void setSaslEncoder(Encoder encoder) {
+        saslTypeEncoder = encoder;
+    }
+
+    /**
+     * Sets an {@link Decoder} instance that will be returned from all calls to the
+     * {@link CodecFactory#getSaslDecoder()}.  If no {@link Decoder} is configured then the
+     * calls to get an Decoder instance will return the default Decoder from the library.
+     * The Decoder configured should only decode the SASL AMQP types.
+     *
+     * @param decoder
+     *      The decoder to return from all calls to the {@link CodecFactory#getSaslDecoder()} method/
+     */
+    public static void setSaslDecoder(Decoder decoder) {
+        saslTypeDecoder = decoder;
+    }
+
+    public static Encoder getEncoder() {
+        if (amqpTypeEncoder == null) {
+            amqpTypeEncoder = getDefaultEncoder();
+        }
+
+        return amqpTypeEncoder;
+    }
+
+    public static Decoder getDecoder() {
+        if (amqpTypeDecoder == null) {
+            amqpTypeDecoder = getDefaultDecoder();
+        }
+
+        return amqpTypeDecoder;
+    }
+
+    public static Encoder getSaslEncoder() {
+        if (saslTypeEncoder == null) {
+            saslTypeEncoder = getDefaultSaslEncoder();
+        }
+
+        return saslTypeEncoder;
+    }
+
+    public static Decoder getSaslDecoder() {
+        if (saslTypeDecoder == null) {
+            saslTypeDecoder = getDefaultSaslDecoder();
+        }
+
+        return saslTypeDecoder;
+    }
+
+    public static Encoder getDefaultEncoder() {
+        return ProtonEncoderFactory.create();
+    }
+
+    public static Decoder getDefaultDecoder() {
+        return ProtonDecoderFactory.create();
+    }
+
+    public static Encoder getDefaultSaslEncoder() {
+        return ProtonEncoderFactory.createSasl();
+    }
+
+    public static Decoder getDefaultSaslDecoder() {
+        return ProtonDecoderFactory.createSasl();
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecodeEOFException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecodeEOFException.java
new file mode 100644
index 0000000..2924d5d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecodeEOFException.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+/**
+ * Exception thrown when a type decoder fails due to some unrecoverable error or
+ * violation of AMQP type specifications for an incoming encoded byte sequence.
+ */
+public class DecodeEOFException extends IllegalArgumentException {
+
+    private static final long serialVersionUID = 5579043130487516118L;
+
+    /**
+     * Creates a generic {@link DecodeEOFException} with no cause or error description set.
+     */
+    public DecodeEOFException() {
+    }
+
+    /**
+     * Creates a {@link DecodeEOFException} with the given error message.
+     *
+     * @param message
+     *      The error message to convey with the exception.
+     */
+    public DecodeEOFException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a {@link DecodeEOFException} with the given assigned root cause exception.
+     *
+     * @param cause
+     *      The underlying exception that triggered this encode error to be thrown.
+     */
+    public DecodeEOFException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Creates a {@link DecodeEOFException} with the given error message and assigned
+     * root cause exception.
+     *
+     * @param message
+     *      The error message to convey with the exception.
+     * @param cause
+     *      The underlying exception that triggered this encode error to be thrown.
+     */
+    public DecodeEOFException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecodeException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecodeException.java
new file mode 100644
index 0000000..85005eb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecodeException.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+/**
+ * Exception thrown when a type decoder fails due to some unrecoverable error or
+ * violation of AMQP type specifications for an incoming encoded byte sequence.
+ */
+public class DecodeException extends IllegalArgumentException {
+
+    private static final long serialVersionUID = 5579043130487516118L;
+
+    /**
+     * Creates a generic {@link DecodeException} with no cause or error description set.
+     */
+    public DecodeException() {
+    }
+
+    /**
+     * Creates a {@link DecodeException} with the given error message.
+     *
+     * @param message
+     *      The error message to convey with the exception.
+     */
+    public DecodeException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a {@link DecodeException} with the given assigned root cause exception.
+     *
+     * @param cause
+     *      The underlying exception that triggered this encode error to be thrown.
+     */
+    public DecodeException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Creates a {@link DecodeException} with the given error message and assigned
+     * root cause exception.
+     *
+     * @param message
+     *      The error message to convey with the exception.
+     * @param cause
+     *      The underlying exception that triggered this encode error to be thrown.
+     */
+    public DecodeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/Decoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/Decoder.java
new file mode 100644
index 0000000..62cb610
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/Decoder.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * Decode AMQP types from a byte stream
+ */
+public interface Decoder {
+
+    /**
+     * Creates a new {@link DecoderState} instance that can be used when interacting with the
+     * Decoder.  For decoding that occurs on more than one thread while sharing a single
+     * {@link Decoder} instance a different state object per thread is required as the
+     * {@link DecoderState} object can retain some state information during the decode process
+     * that could be corrupted if more than one thread were to share a single instance.
+     *
+     * For single threaded decoding work the {@link Decoder} offers a utility
+     * cached {@link DecoderState} API that will return the same instance on each call which can
+     * reduce allocation overhead and make using the {@link Decoder} simpler.
+     *
+     * @return a newly constructed {@link EncoderState} instance.
+     */
+    DecoderState newDecoderState();
+
+    /**
+     * Return a singleton {@link DecoderState} instance that is meant to be shared within single threaded
+     * decoder interactions.  If more than one thread makes use of this cached {@link DecoderState} the
+     * results of any decoding done using this state object is not guaranteed to be correct.  The returned
+     * instance will have its reset method called to ensure that any previously stored state data is cleared
+     * before the next use.
+     *
+     * @return a cached {@link DecoderState} linked to this Decoder instance that has been reset.
+     */
+    DecoderState getCachedDecoderState();
+
+    Boolean readBoolean(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    boolean readBoolean(ProtonBuffer buffer, DecoderState state, boolean defaultValue) throws DecodeException;
+
+    Byte readByte(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    byte readByte(ProtonBuffer buffer, DecoderState state, byte defaultValue) throws DecodeException;
+
+    UnsignedByte readUnsignedByte(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    byte readUnsignedByte(ProtonBuffer buffer, DecoderState state, byte defaultValue) throws DecodeException;
+
+    Character readCharacter(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    char readCharacter(ProtonBuffer buffer, DecoderState state, char defaultValue) throws DecodeException;
+
+    Decimal32 readDecimal32(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    Decimal64 readDecimal64(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    Decimal128 readDecimal128(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    Short readShort(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    short readShort(ProtonBuffer buffer, DecoderState state, short defaultValue) throws DecodeException;
+
+    UnsignedShort readUnsignedShort(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    short readUnsignedShort(ProtonBuffer buffer, DecoderState state, short defaultValue) throws DecodeException;
+
+    int readUnsignedShort(ProtonBuffer buffer, DecoderState state, int defaultValue) throws DecodeException;
+
+    Integer readInteger(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    int readInteger(ProtonBuffer buffer, DecoderState state, int defaultValue) throws DecodeException;
+
+    UnsignedInteger readUnsignedInteger(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    int readUnsignedInteger(ProtonBuffer buffer, DecoderState state, int defaultValue) throws DecodeException;
+
+    long readUnsignedInteger(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException;
+
+    Long readLong(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    long readLong(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException;
+
+    UnsignedLong readUnsignedLong(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    long readUnsignedLong(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException;
+
+    Float readFloat(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    float readFloat(ProtonBuffer buffer, DecoderState state, float defaultValue) throws DecodeException;
+
+    Double readDouble(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    double readDouble(ProtonBuffer buffer, DecoderState state, double defaultValue) throws DecodeException;
+
+    Binary readBinary(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    ProtonBuffer readBinaryAsBuffer(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    /**
+     * This method expects to read a {@link Binary} encoded type from the provided buffer and
+     * constructs a {@link DeliveryTag} type that wraps the bytes encoded.  If the encoding is
+     * a NULL AMQP type then this method returns <code>null</code>.
+     *
+     * @param buffer
+     *      The buffer to read a Binary encoded value from
+     * @param state
+     *      The current encoding state.
+     *
+     * @return a new DeliveryTag instance or null if an AMQP NULL encoding is found.
+     *
+     * @throws DecodeException if an error occurs while decoding the {@link DeliveryTag} instance.
+     */
+    DeliveryTag readDeliveryTag(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    String readString(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    Symbol readSymbol(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    String readSymbol(ProtonBuffer buffer, DecoderState state, String defaultValue) throws DecodeException;
+
+    Long readTimestamp(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    long readTimestamp(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException;
+
+    UUID readUUID(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    Object readObject(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    <T> T readObject(ProtonBuffer buffer, DecoderState state, final Class<T> clazz) throws DecodeException;
+
+    <T> T[] readMultiple(ProtonBuffer buffer, DecoderState state, final Class<T> clazz) throws DecodeException;
+
+    <K,V> Map<K, V> readMap(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    <V> List<V> readList(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    TypeDecoder<?> readNextTypeDecoder(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    TypeDecoder<?> peekNextTypeDecoder(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    <V> Decoder registerDescribedTypeDecoder(DescribedTypeDecoder<V> decoder);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecoderState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecoderState.java
new file mode 100644
index 0000000..5d63829
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DecoderState.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Retains state of decode either between calls or across decode iterations
+ */
+public interface DecoderState {
+
+    /**
+     * Resets any intermediate state back to default values.
+     *
+     * @return this {@link DecoderState} instance.
+     */
+    DecoderState reset();
+
+    /**
+     * @return the decoder that created this state object
+     */
+    Decoder getDecoder();
+
+    /**
+     * Given a set of UTF-8 encoded bytes decode and return the String that
+     * represents that UTF-8 value.
+     *
+     * @param buffer
+     *      A buffer containing the UTF-8 encoded bytes to be decoded.
+     * @param length
+     *      The number of bytes in the passed buffer that comprise the UTF-8 encoding.
+     *
+     * @return a String that represents the UTF-8 decoded bytes.
+     *
+     * @throws DecodeException if an error occurs while decoding the string value.
+     */
+    String decodeUTF8(ProtonBuffer buffer, int length) throws DecodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DescribedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DescribedTypeDecoder.java
new file mode 100644
index 0000000..d8ff14a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DescribedTypeDecoder.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Interface for all DescribedType decoder implementations
+ *
+ * @param <V> The type this decoder handles
+ */
+public interface DescribedTypeDecoder<V> extends TypeDecoder<V> {
+
+    /**
+     * Returns the AMQP descriptor code for the type this decoder reads.
+     *
+     * @return an unsigned long descriptor code value.
+     */
+    UnsignedLong getDescriptorCode();
+
+    /**
+     * Returns the AMQP descriptor symbol for the type this decoder reads.
+     *
+     * @return an symbol descriptor code value.
+     */
+    Symbol getDescriptorSymbol();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DescribedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DescribedTypeEncoder.java
new file mode 100644
index 0000000..0855143
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/DescribedTypeEncoder.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Interface for all DescribedType encoder implementations
+ *
+ * @param <V> The type this decoder handles
+ */
+public interface DescribedTypeEncoder<V> extends TypeEncoder<V> {
+
+    /**
+     * @return the UnsignedLong value to use as the Descriptor for this type.
+     */
+    UnsignedLong getDescriptorCode();
+
+    /**
+     * @return the Symbol value to use as the Descriptor for this type.
+     */
+    Symbol getDescriptorSymbol();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncodeException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncodeException.java
new file mode 100644
index 0000000..510a5f0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncodeException.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+/**
+ * Exception thrown when a type encoder fails due to some unrecoverable error or
+ * violation of AMQP type specifications.
+ */
+public class EncodeException extends IllegalArgumentException {
+
+    private static final long serialVersionUID = 4909721739062393272L;
+
+    /**
+     * Creates a generic {@link EncodeException} with no cause or error description set.
+     */
+    public EncodeException() {
+    }
+
+    /**
+     * Creates a {@link EncodeException} with the given error message.
+     *
+     * @param message
+     *      The error message to convey with the exception.
+     */
+    public EncodeException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a {@link EncodeException} with the given assigned root cause exception.
+     *
+     * @param cause
+     *      The underlying exception that triggered this encode error to be thrown.
+     */
+    public EncodeException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Creates a {@link EncodeException} with the given error message and assigned
+     * root cause exception.
+     *
+     * @param message
+     *      The error message to convey with the exception.
+     * @param cause
+     *      The underlying exception that triggered this encode error to be thrown.
+     */
+    public EncodeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/Encoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/Encoder.java
new file mode 100644
index 0000000..9f37772
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/Encoder.java
@@ -0,0 +1,233 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.DescribedType;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * Encode AMQP types into binary streams
+ */
+public interface Encoder {
+
+    /**
+     * Creates a new {@link EncoderState} instance that can be used when interacting with the
+     * Encoder.  For encoding that occurs on more than one thread while sharing a single
+     * {@link Encoder} instance a different state object per thread is required as the
+     * {@link EncoderState} object can retain some state information during the encode process
+     * that could be corrupted if more than one thread were to share a single instance.
+     *
+     * For single threaded encoding work the {@link Encoder} offers a utility
+     * cached {@link EncoderState} API that will return the same instance on each call which can
+     * reduce allocation overhead and make using the {@link Encoder} simpler.
+     *
+     * @return a newly constructed {@link EncoderState} instance.
+     */
+    EncoderState newEncoderState();
+
+    /**
+     * Return a singleton {@link EncoderState} instance that is meant to be shared within single threaded
+     * encoder interactions.  If more than one thread makes use of this cache {@link EncoderState} the
+     * results of any encoding done using this state object is not guaranteed to be correct.  The returned
+     * instance will have its reset method called to ensure that any previously stored state data is cleared
+     * before the next use.
+     *
+     * @return a cached {@link EncoderState} linked to this Encoder instance.
+     */
+    EncoderState getCachedEncoderState();
+
+    void writeNull(ProtonBuffer buffer, EncoderState state) throws EncodeException;
+
+    void writeBoolean(ProtonBuffer buffer, EncoderState state, boolean value) throws EncodeException;
+
+    void writeBoolean(ProtonBuffer buffer, EncoderState state, Boolean value) throws EncodeException;
+
+    void writeUnsignedByte(ProtonBuffer buffer, EncoderState state, UnsignedByte value) throws EncodeException;
+
+    void writeUnsignedByte(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException;
+
+    void writeUnsignedShort(ProtonBuffer buffer, EncoderState state, UnsignedShort value) throws EncodeException;
+
+    void writeUnsignedShort(ProtonBuffer buffer, EncoderState state, short value) throws EncodeException;
+
+    void writeUnsignedShort(ProtonBuffer buffer, EncoderState state, int value) throws EncodeException;
+
+    void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, UnsignedInteger value) throws EncodeException;
+
+    void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException;
+
+    void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, int value) throws EncodeException;
+
+    void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException;
+
+    void writeUnsignedLong(ProtonBuffer buffer, EncoderState state, UnsignedLong value) throws EncodeException;
+
+    void writeUnsignedLong(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException;
+
+    void writeUnsignedLong(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException;
+
+    void writeByte(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException;
+
+    void writeByte(ProtonBuffer buffer, EncoderState state, Byte value) throws EncodeException;
+
+    void writeShort(ProtonBuffer buffer, EncoderState state, short value) throws EncodeException;
+
+    void writeShort(ProtonBuffer buffer, EncoderState state, Short value) throws EncodeException;
+
+    void writeInteger(ProtonBuffer buffer, EncoderState state, int value) throws EncodeException;
+
+    void writeInteger(ProtonBuffer buffer, EncoderState state, Integer value) throws EncodeException;
+
+    void writeLong(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException;
+
+    void writeLong(ProtonBuffer buffer, EncoderState state, Long value) throws EncodeException;
+
+    void writeFloat(ProtonBuffer buffer, EncoderState state, float value) throws EncodeException;
+
+    void writeFloat(ProtonBuffer buffer, EncoderState state, Float value) throws EncodeException;
+
+    void writeDouble(ProtonBuffer buffer, EncoderState state, double value) throws EncodeException;
+
+    void writeDouble(ProtonBuffer buffer, EncoderState state, Double value) throws EncodeException;
+
+    void writeDecimal32(ProtonBuffer buffer, EncoderState state, Decimal32 value) throws EncodeException;
+
+    void writeDecimal64(ProtonBuffer buffer, EncoderState state, Decimal64 value) throws EncodeException;
+
+    void writeDecimal128(ProtonBuffer buffer, EncoderState state, Decimal128 value) throws EncodeException;
+
+    void writeCharacter(ProtonBuffer buffer, EncoderState state, char value) throws EncodeException;
+
+    void writeCharacter(ProtonBuffer buffer, EncoderState state, Character value) throws EncodeException;
+
+    void writeTimestamp(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException;
+
+    void writeTimestamp(ProtonBuffer buffer, EncoderState state, Date value) throws EncodeException;
+
+    void writeUUID(ProtonBuffer buffer, EncoderState state, UUID value) throws EncodeException;
+
+    void writeBinary(ProtonBuffer buffer, EncoderState state, Binary value) throws EncodeException;
+
+    /**
+     * Writes the contents of the given {@link ProtonBuffer} value into the provided {@link ProtonBuffer}
+     * instance as an AMQP Binary type.  This method does not modify the read index of the value given such
+     * that is can be read later or written again without needing to reset the read index manually.
+     * <p>
+     * If the provided value to write is null an AMQP null type is encoded into the target buffer.
+     *
+     * @param buffer
+     *      the target buffer where the binary value is to be encoded
+     * @param state
+     *      the {@link EncoderState} instance that manages the calling threads state tracking.
+     * @param value
+     *      the {@link ProtonBuffer} value to be encoded as an AMQP binary instance.
+     *
+     * @throws EncodeException if an error occurs while performing the encode
+     */
+    void writeBinary(ProtonBuffer buffer, EncoderState state, ProtonBuffer value) throws EncodeException;
+
+    void writeBinary(ProtonBuffer buffer, EncoderState state, byte[] value) throws EncodeException;
+
+    void writeString(ProtonBuffer buffer, EncoderState state, String value) throws EncodeException;
+
+    void writeSymbol(ProtonBuffer buffer, EncoderState state, Symbol value) throws EncodeException;
+
+    void writeSymbol(ProtonBuffer buffer, EncoderState state, String value) throws EncodeException;
+
+    <T> void writeList(ProtonBuffer buffer, EncoderState state, List<T> value) throws EncodeException;
+
+    <K, V> void writeMap(ProtonBuffer buffer, EncoderState state, Map<K, V> value) throws EncodeException;
+
+    /**
+     * Writes the contents of the given {@link DeliveryTag} value into the provided {@link ProtonBuffer}
+     * instance as an AMQP Binary type.
+     * <p>
+     * If the provided value to write is null an AMQP null type is encoded into the target buffer.
+     *
+     * @param buffer
+     *      the target buffer where the binary value is to be encoded
+     * @param state
+     *      the {@link EncoderState} instance that manages the calling threads state tracking.
+     * @param value
+     *      the {@link DeliveryTag} value to be encoded as an AMQP binary instance.
+     *
+     * @throws EncodeException if an error occurs while performing the encode
+     */
+    void writeDeliveryTag(ProtonBuffer buffer, EncoderState state, DeliveryTag value) throws EncodeException;
+
+    void writeDescribedType(ProtonBuffer buffer, EncoderState state, DescribedType value) throws EncodeException;
+
+    void writeObject(ProtonBuffer buffer, EncoderState state, Object value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, boolean[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, byte[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, short[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, int[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, long[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, float[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, double[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, char[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, Object[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, Decimal32[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, Decimal64[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, Decimal128[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, Symbol[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedByte[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedShort[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedInteger[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedLong[] value) throws EncodeException;
+
+    void writeArray(ProtonBuffer buffer, EncoderState state, UUID[] value) throws EncodeException;
+
+    <V> Encoder registerDescribedTypeEncoder(DescribedTypeEncoder<V> encoder) throws EncodeException;
+
+    TypeEncoder<?> getTypeEncoder(Object value);
+
+    TypeEncoder<?> getTypeEncoder(Class<?> typeClass);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncoderState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncoderState.java
new file mode 100644
index 0000000..db07313
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncoderState.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Retains Encoder state information either between calls or across encode iterations.
+ */
+public interface EncoderState {
+
+    /**
+     * @return the Encoder instance that create this state object.
+     */
+    Encoder getEncoder();
+
+    /**
+     * Resets any intermediate state back to default values.
+     *
+     * @return this {@link EncoderState} instance.
+     */
+    EncoderState reset();
+
+    /**
+     * Encodes the given sequence of characters in UTF8 to the given buffer.
+     *
+     * @param buffer
+     *      A ProtonBuffer where the UTF-8 encoded bytes should be written.
+     * @param sequence
+     *      A {@link CharSequence} representing the UTF-8 bytes to encode
+     *
+     * @return a reference to the encoding buffer for chaining
+     *
+     * @throws EncodeException if an error occurs while encoding the {@link CharSequence}
+     */
+    ProtonBuffer encodeUTF8(ProtonBuffer buffer, CharSequence sequence) throws EncodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncodingCodes.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncodingCodes.java
new file mode 100644
index 0000000..dd6bba3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/EncodingCodes.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+public interface EncodingCodes {
+
+    public static final byte DESCRIBED_TYPE_INDICATOR = (byte) 0x00;
+
+    public static final byte NULL                     = (byte) 0x40;
+
+    public static final byte BOOLEAN                  = (byte) 0x56;
+    public static final byte BOOLEAN_TRUE             = (byte) 0x41;
+    public static final byte BOOLEAN_FALSE            = (byte) 0x42;
+
+    public static final byte UBYTE                    = (byte) 0x50;
+
+    public static final byte USHORT                   = (byte) 0x60;
+
+    public static final byte UINT                     = (byte) 0x70;
+    public static final byte SMALLUINT                = (byte) 0x52;
+    public static final byte UINT0                    = (byte) 0x43;
+
+    public static final byte ULONG                    = (byte) 0x80;
+    public static final byte SMALLULONG               = (byte) 0x53;
+    public static final byte ULONG0                   = (byte) 0x44;
+
+    public static final byte BYTE                     = (byte) 0x51;
+
+    public static final byte SHORT                    = (byte) 0x61;
+
+    public static final byte INT                      = (byte) 0x71;
+    public static final byte SMALLINT                 = (byte) 0x54;
+
+    public static final byte LONG                     = (byte) 0x81;
+    public static final byte SMALLLONG                = (byte) 0x55;
+
+    public static final byte FLOAT                    = (byte) 0x72;
+
+    public static final byte DOUBLE                   = (byte) 0x82;
+
+    public static final byte DECIMAL32                = (byte) 0x74;
+
+    public static final byte DECIMAL64                = (byte) 0x84;
+
+    public static final byte DECIMAL128               = (byte) 0x94;
+
+    public static final byte CHAR                     = (byte) 0x73;
+
+    public static final byte TIMESTAMP                = (byte) 0x83;
+
+    public static final byte UUID                     = (byte) 0x98;
+
+    public static final byte VBIN8                    = (byte) 0xa0;
+    public static final byte VBIN32                   = (byte) 0xb0;
+
+    public static final byte STR8                     = (byte) 0xa1;
+    public static final byte STR32                    = (byte) 0xb1;
+
+    public static final byte SYM8                     = (byte) 0xa3;
+    public static final byte SYM32                    = (byte) 0xb3;
+
+    public static final byte LIST0                    = (byte) 0x45;
+    public static final byte LIST8                    = (byte) 0xc0;
+    public static final byte LIST32                   = (byte) 0xd0;
+
+    public static final byte MAP8                     = (byte) 0xc1;
+    public static final byte MAP32                    = (byte) 0xd1;
+
+    public static final byte ARRAY8                   = (byte) 0xe0;
+    public static final byte ARRAY32                  = (byte) 0xf0;
+
+    static String toString(byte encoding) {
+        switch (encoding) {
+            case DESCRIBED_TYPE_INDICATOR:
+                return "DESCRIBED_TYPE_INDICATOR:0x00";
+            case NULL:
+                return "NULL:0x40";
+            case BOOLEAN:
+                return "BOOLEAN:0x56";
+            case BOOLEAN_TRUE:
+                return "BOOLEAN_TRUE:0x41";
+            case BOOLEAN_FALSE:
+                return "BOOLEAN_FALSE:0x42";
+            case UBYTE:
+                return "UBYTE:0x50";
+            case USHORT:
+                return "USHORT:0x60";
+            case UINT:
+                return "UINT:0x70";
+            case SMALLUINT:
+                return "SMALLUINT:0x52";
+            case UINT0:
+                return "UINT0:0x43";
+            case ULONG:
+                return "ULONG:0x80";
+            case SMALLULONG:
+                return "SMALLULONG:0x53";
+            case ULONG0:
+                return "ULONG0:0x44";
+            case BYTE:
+                return "BYTE:0x51";
+            case SHORT:
+                return "SHORT:0x61";
+            case INT:
+                return "INT:0x71";
+            case SMALLINT:
+                return "SMALLINT:0x54";
+            case LONG:
+                return "LONG:0x81";
+            case SMALLLONG:
+                return "SMALLLONG:0x55";
+            case FLOAT:
+                return "FLOAT:0x72";
+            case DOUBLE:
+                return "DOUBLE:0x82";
+            case DECIMAL32:
+                return "DECIMAL32:0x74";
+            case DECIMAL64:
+                return "DECIMAL64:0x84";
+            case DECIMAL128:
+                return "DECIMAL128:0x94";
+            case CHAR:
+                return "CHAR:0x73";
+            case TIMESTAMP:
+                return "TIMESTAMP:0x83";
+            case UUID:
+                return "UUID:0x98";
+            case VBIN8:
+                return "VBIN8:0xa0";
+            case VBIN32:
+                return "VBIN32:0xb0";
+            case STR8:
+                return "STR8:0xa1";
+            case STR32:
+                return "STR32:0xb1";
+            case SYM8:
+                return "SYM8:0xa3";
+            case SYM32:
+                return "SYM32:0xb3";
+            case LIST0:
+                return "LIST0:0x45";
+            case LIST8:
+                return "LIST8:0xc0";
+            case LIST32:
+                return "LIST32:0xd0";
+            case MAP8:
+                return "MAP8:0xc1";
+            case MAP32:
+                return "MAP32:0xd1";
+            case ARRAY8:
+                return "ARRAY32:0xe0";
+            case ARRAY32:
+                return "ARRAY32:0xf0";
+            default:
+                return "Unknown-Type:" + String.format("0x%02X ", encoding);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDecoder.java
new file mode 100644
index 0000000..095c5d6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDecoder.java
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * Decode AMQP types from a {@link InputStream}
+ */
+public interface StreamDecoder {
+
+    /**
+     * Creates a new {@link StreamDecoderState} instance that can be used when interacting with the
+     * Decoder.  For decoding that occurs on more than one thread while sharing a single
+     * {@link StreamDecoder} instance a different state object per thread is required as the
+     * {@link StreamDecoderState} object can retain some state information during the decode process
+     * that could be corrupted if more than one thread were to share a single instance.
+     *
+     * For single threaded decoding work the {@link StreamDecoder} offers a utility
+     * cached {@link StreamDecoderState} API that will return the same instance on each call which can
+     * reduce allocation overhead and make using the {@link StreamDecoder} simpler.
+     *
+     * @return a newly constructed {@link EncoderState} instance.
+     */
+    StreamDecoderState newDecoderState();
+
+    /**
+     * Return a singleton {@link StreamDecoderState} instance that is meant to be shared within single threaded
+     * decoder interactions.  If more than one thread makes use of this cached {@link StreamDecoderState} the
+     * results of any decoding done using this state object is not guaranteed to be correct.  The returned
+     * instance will have its reset method called to ensure that any previously stored state data is cleared
+     * before the next use.
+     *
+     * @return a cached {@link StreamDecoderState} linked to this Decoder instance that has been reset.
+     */
+    StreamDecoderState getCachedDecoderState();
+
+    Boolean readBoolean(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    boolean readBoolean(InputStream stream, StreamDecoderState state, boolean defaultValue) throws DecodeException;
+
+    Byte readByte(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    byte readByte(InputStream stream, StreamDecoderState state, byte defaultValue) throws DecodeException;
+
+    UnsignedByte readUnsignedByte(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    byte readUnsignedByte(InputStream stream, StreamDecoderState state, byte defaultValue) throws DecodeException;
+
+    Character readCharacter(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    char readCharacter(InputStream stream, StreamDecoderState state, char defaultValue) throws DecodeException;
+
+    Decimal32 readDecimal32(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    Decimal64 readDecimal64(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    Decimal128 readDecimal128(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    Short readShort(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    short readShort(InputStream stream, StreamDecoderState state, short defaultValue) throws DecodeException;
+
+    UnsignedShort readUnsignedShort(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    short readUnsignedShort(InputStream stream, StreamDecoderState state, short defaultValue) throws DecodeException;
+
+    int readUnsignedShort(InputStream stream, StreamDecoderState state, int defaultValue) throws DecodeException;
+
+    Integer readInteger(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    int readInteger(InputStream stream, StreamDecoderState state, int defaultValue) throws DecodeException;
+
+    UnsignedInteger readUnsignedInteger(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    int readUnsignedInteger(InputStream stream, StreamDecoderState state, int defaultValue) throws DecodeException;
+
+    long readUnsignedInteger(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException;
+
+    Long readLong(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    long readLong(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException;
+
+    UnsignedLong readUnsignedLong(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    long readUnsignedLong(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException;
+
+    Float readFloat(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    float readFloat(InputStream stream, StreamDecoderState state, float defaultValue) throws DecodeException;
+
+    Double readDouble(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    double readDouble(InputStream stream, StreamDecoderState state, double defaultValue) throws DecodeException;
+
+    Binary readBinary(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    ProtonBuffer readBinaryAsBuffer(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    /**
+     * This method expects to read a {@link Binary} encoded type from the provided buffer and
+     * constructs a {@link DeliveryTag} type that wraps the bytes encoded.  If the encoding is
+     * a NULL AMQP type then this method returns <code>null</code>.
+     *
+     * @param stream
+     *      The {@link InputStream} to read a Binary encoded value from
+     * @param state
+     *      The current encoding state.
+     *
+     * @return a new DeliveryTag instance or null if an AMQP NULL encoding is found.
+     *
+     * @throws DecodeException if an error occurs while decoding the {@link DeliveryTag} instance.
+     */
+    DeliveryTag readDeliveryTag(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    String readString(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    Symbol readSymbol(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    String readSymbol(InputStream stream, StreamDecoderState state, String defaultValue) throws DecodeException;
+
+    Long readTimestamp(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    long readTimestamp(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException;
+
+    UUID readUUID(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    Object readObject(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    <T> T readObject(InputStream stream, StreamDecoderState state, final Class<T> clazz) throws DecodeException;
+
+    <T> T[] readMultiple(InputStream stream, StreamDecoderState state, final Class<T> clazz) throws DecodeException;
+
+    <K,V> Map<K, V> readMap(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    <V> List<V> readList(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    StreamTypeDecoder<?> readNextTypeDecoder(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    StreamTypeDecoder<?> peekNextTypeDecoder(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    <V> StreamDecoder registerDescribedTypeDecoder(StreamDescribedTypeDecoder<V> decoder);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDecoderState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDecoderState.java
new file mode 100644
index 0000000..99ce710
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDecoderState.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import java.io.InputStream;
+
+/**
+ * Retains state of the {@link InputStream} based decode either between calls or across decode iterations
+ */
+public interface StreamDecoderState {
+
+    /**
+     * Resets any intermediate state back to default values.
+     *
+     * @return this {@link StreamDecoderState} instance.
+     */
+    StreamDecoderState reset();
+
+    /**
+     * @return the {@link StreamDecoder} that created this state object
+     */
+    StreamDecoder getDecoder();
+
+    /**
+     * Given a stream that will provide UTF-8 encoded bytes, decode and return the String that
+     * represents that UTF-8 value.
+     *
+     * @param stream
+     *      A stream from which the UTF-8 encoded bytes are to be decoded.
+     * @param length
+     *      The number of bytes in the passed {@link InputStream} that comprise the UTF-8 encoding.
+     *
+     * @return a String that represents the UTF-8 decoded bytes.
+     *
+     * @throws DecodeException if an error occurs while decoding the string value.
+     */
+    String decodeUTF8(InputStream stream, int length) throws DecodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDescribedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDescribedTypeDecoder.java
new file mode 100644
index 0000000..c4f04d3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamDescribedTypeDecoder.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Interface for all DescribedType decoder implementations
+ *
+ * @param <V> The type this decoder handles
+ */
+public interface StreamDescribedTypeDecoder<V> extends StreamTypeDecoder<V> {
+
+    /**
+     * Returns the AMQP descriptor code for the type this decoder reads.
+     *
+     * @return an unsigned long descriptor code value.
+     */
+    UnsignedLong getDescriptorCode();
+
+    /**
+     * Returns the AMQP descriptor symbol for the type this decoder reads.
+     *
+     * @return an symbol descriptor code value.
+     */
+    Symbol getDescriptorSymbol();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamTypeDecoder.java
new file mode 100644
index 0000000..94651e1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/StreamTypeDecoder.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import java.io.InputStream;
+
+/**
+ * Interface for an decoder of a specific AMQP Type.
+ *
+ * @param <V> The type that will be returned when this decoder reads a value.
+ */
+public interface StreamTypeDecoder<V> {
+
+    /**
+     * @return the Class that this decoder handles.
+     */
+    Class<V> getTypeClass();
+
+    /**
+     * @return true if the underlying type that is going to be decoded is an array type
+     */
+    boolean isArrayType();
+
+    /**
+     * Reads the next type from the given buffer and returns it.
+     *
+     * @param stream
+     * 		the source of encoded data.
+     * @param state
+     * 		the current state of the decoder.
+     *
+     * @return the next instance in the stream that this decoder handles.
+     *
+     * @throws DecodeException if an error is encountered while reading the next value.
+     */
+    V readValue(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    /**
+     * Skips over the bytes that compose the type this descriptor decodes.
+     * <p>
+     * Skipping values can be used when the type is not used or processed by the
+     * application doing the decoding.  An example might be an AMQP message decoder
+     * that only needs to decode certain parts of the message and not others.
+     *
+     * @param stream
+     *      The stream that contains the encoded type.
+     * @param state
+     *      The decoder state.
+     *
+     * @throws DecodeException if an error occurs while skipping the value.
+     */
+    void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException;
+
+    /**
+     * Reads a series of this type that have been encoded into the body of an Array type.
+     * <p>
+     * When encoded into an array the values are encoded in series following the identifier
+     * for the type, this method is given a count of the number of instances that are encoded
+     * and should read each in succession and returning them in a new array.
+     *
+     * @param stream
+     *      the source of encoded data.
+     * @param state
+     *      the current state of the decoder.
+     * @param count
+     *      the number of array elements encoded in the buffer.
+     *
+     * @return the next instance in the stream that this decoder handles.
+     *
+     * @throws DecodeException if an error is encountered while reading the next value.
+     */
+    V[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/TypeDecoder.java
new file mode 100644
index 0000000..9d4504e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/TypeDecoder.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Interface for an decoder of a specific AMQP Type.
+ *
+ * @param <V> The type that will be returned when this decoder reads a value.
+ */
+public interface TypeDecoder<V> {
+
+    /**
+     * @return the Class that this decoder handles.
+     */
+    Class<V> getTypeClass();
+
+    /**
+     * @return true if the underlying type that is going to be decoded is an array type
+     */
+    boolean isArrayType();
+
+    /**
+     * Reads the next type from the given buffer and returns it.
+     *
+     * @param buffer
+     * 		the source of encoded data.
+     * @param state
+     * 		the current state of the decoder.
+     *
+     * @return the next instance in the stream that this decoder handles.
+     *
+     * @throws DecodeException if an error is encountered while reading the next value.
+     */
+    V readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    /**
+     * Skips over the bytes that compose the type this descriptor decodes.
+     * <p>
+     * Skipping values can be used when the type is not used or processed by the
+     * application doing the decoding.  An example might be an AMQP message decoder
+     * that only needs to decode certain parts of the message and not others.
+     *
+     * @param buffer
+     *      The buffer that contains the encoded type.
+     * @param state
+     *      The decoder state.
+     *
+     * @throws DecodeException if an error occurs while skipping the value.
+     */
+    void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException;
+
+    /**
+     * Reads a series of this type that have been encoded into the body of an Array type.
+     * <p>
+     * When encoded into an array the values are encoded in series following the identifier
+     * for the type, this method is given a count of the number of instances that are encoded
+     * and should read each in succession and returning them in a new array.
+     *
+     * @param buffer
+     *      the source of encoded data.
+     * @param state
+     *      the current state of the decoder.
+     * @param count
+     *      the number of array elements encoded in the buffer.
+     *
+     * @return the next instance in the stream that this decoder handles.
+     *
+     * @throws DecodeException if an error is encountered while reading the next value.
+     */
+    V[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/TypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/TypeEncoder.java
new file mode 100644
index 0000000..5c8f3ee
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/TypeEncoder.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Interface for an encoder of a specific AMQP Type.
+ *
+ * @param <V> the concrete Type that this encoder handles.
+ */
+public interface TypeEncoder<V> {
+
+    /**
+     * @return the Class type that this encoder handles.
+     */
+    Class<V> getTypeClass();
+
+    /**
+     * @return true if the type handled by this encoded is an AMQP Array type.
+     */
+    boolean isArrayType();
+
+    /**
+     * Write the full AMQP type data to the given byte buffer.
+     * <p>
+     * This can consist of writing both a type constructor value and
+     * the bytes that make up the value of the type being written.
+     *
+     * @param buffer
+     * 		The buffer to write the AMQP type to
+     * @param state
+     * 		The current encoder state
+     * @param value
+     * 		The value that is to be written.
+     *
+     * @throws EncodeException if an error occurs while encoding the given value.
+     */
+    void writeType(ProtonBuffer buffer, EncoderState state, V value) throws EncodeException;
+
+    /**
+     * Write an array elements of the AMQP type to the given byte buffer.
+     * <p>
+     * This method writes the full Array type definition of an array of the
+     * type this encoder manages.
+     *
+     * @param buffer
+     *      The buffer to write the AMQP array elements to
+     * @param state
+     *      The current encoder state
+     * @param values
+     *      The array of values that is to be written.
+     *
+     * @throws EncodeException if an error occurs while encoding the given value.
+     */
+    void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) throws EncodeException;
+
+    /**
+     * Write an array elements of the AMQP type to the given byte buffer.
+     * <p>
+     * This method writes only the body portion of an AMQP array of this type, the
+     * array encoding, size and element count should be assumed to be managed by
+     * the caller.
+     *
+     * @param buffer
+     *      The buffer to write the AMQP array elements to
+     * @param state
+     *      The current encoder state
+     * @param values
+     *      The array of values that is to be written.
+     *
+     * @throws EncodeException if an error occurs while encoding the given value.
+     */
+    void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) throws EncodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/AbstractDescribedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/AbstractDescribedTypeDecoder.java
new file mode 100644
index 0000000..7928e84
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/AbstractDescribedTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.StreamDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+
+/**
+ * Abstract base for all Described Type decoders which implements the generic methods
+ * common to all the implementations.
+ *
+ * @param <V> The type that this decoder handles.
+ */
+public abstract class AbstractDescribedTypeDecoder<V> implements DescribedTypeDecoder<V>, StreamDescribedTypeDecoder<V> {
+
+    @Override
+    public boolean isArrayType() {
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "DescribedTypeDecoder<" + getTypeClass().getSimpleName() + ">";
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static <E> E checkIsExpectedTypeAndCast(Class<?> expected, TypeDecoder<?> actual) throws DecodeException {
+        if (!expected.isAssignableFrom(actual.getClass())) {
+            throw new DecodeException(
+                "Expected " + expected + "encoding but got decoder for type: " + actual.getTypeClass().getName());
+        }
+
+        return (E) expected.cast(actual);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected static <E> E checkIsExpectedTypeAndCast(Class<?> expected, StreamTypeDecoder<?> actual) throws DecodeException {
+        if (!expected.isAssignableFrom(actual.getClass())) {
+            throw new DecodeException(
+                "Expected " + expected + "encoding but got decoder for type: " + actual.getTypeClass().getName());
+        }
+
+        return (E) expected.cast(actual);
+    }
+
+    protected static void checkIsExpectedType(Class<?> expected, TypeDecoder<?> actual) throws DecodeException {
+        if (!expected.isAssignableFrom(actual.getClass())) {
+            throw new DecodeException(
+                "Expected " + expected + "encoding but got decoder for type: " + actual.getTypeClass().getName());
+        }
+    }
+
+    protected static void checkIsExpectedType(Class<?> expected, StreamTypeDecoder<?> actual) throws DecodeException {
+        if (!expected.isAssignableFrom(actual.getClass())) {
+            throw new DecodeException(
+                "Expected " + expected + "encoding but got decoder for type: " + actual.getTypeClass().getName());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/AbstractPrimitiveTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/AbstractPrimitiveTypeDecoder.java
new file mode 100644
index 0000000..f5d99d1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/AbstractPrimitiveTypeDecoder.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.io.InputStream;
+import java.lang.reflect.Array;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+
+/**
+ * Abstract base for all Described Type decoders which implements the generic methods
+ * common to all the implementations.
+ *
+ * @param <V> The type that this primitive decoder handles.
+ */
+public abstract class AbstractPrimitiveTypeDecoder<V> implements PrimitiveTypeDecoder<V> {
+
+    @Override
+    public boolean isArrayType() {
+        return false;
+    }
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return false;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public V[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        V[] array = (V[]) Array.newInstance(getTypeClass(), count);
+        for (int i = 0; i < count; ++i) {
+            array[i] = readValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public V[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        V[] array = (V[]) Array.newInstance(getTypeClass(), count);
+        for (int i = 0; i < count; ++i) {
+            array[i] = readValue(stream, state);
+        }
+
+        return array;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/PrimitiveArrayTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/PrimitiveArrayTypeDecoder.java
new file mode 100644
index 0000000..b6ed90d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/PrimitiveArrayTypeDecoder.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+/**
+ * Provides an interface for an Array type decoder that provides the Proton decoder
+ * with entry points to read arrays in a manner that support the desired Java array
+ * type to be returned.
+ */
+public interface PrimitiveArrayTypeDecoder extends PrimitiveTypeDecoder<Object> {
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/PrimitiveTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/PrimitiveTypeDecoder.java
new file mode 100644
index 0000000..4718ba0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/PrimitiveTypeDecoder.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+
+/**
+ * Interface for a TypeDecoder that manages decoding of AMQP primitive types.
+ *
+ * @param <V> the Type Class that this decoder manages.
+ */
+public interface PrimitiveTypeDecoder<V> extends TypeDecoder<V>, StreamTypeDecoder<V> {
+
+    /**
+     * @return true if the type managed by this decoder is assignable to a Java primitive type.
+     */
+    boolean isJavaPrimitive();
+
+    /**
+     * @return the AMQP Encoding Code that this primitive type decoder can read.
+     */
+    int getTypeCode();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoder.java
new file mode 100644
index 0000000..47d23bb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoder.java
@@ -0,0 +1,940 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.lang.reflect.Array;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeEOFException;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.DescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Array32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Array8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Binary32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Binary8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BooleanFalseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BooleanTrueTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BooleanTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ByteTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.CharacterTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal128TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal64TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.DoubleTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.FloatTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Integer32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Integer8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.List0TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.List32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.List8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Long8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.LongTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Map32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Map8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ShortTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.String32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.String8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Symbol32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Symbol8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.TimestampTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UUIDTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedByteTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedInteger0TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedInteger32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedInteger8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedLong0TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedLong64TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedLong8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedShortTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * The default AMQP Decoder implementation.
+ */
+public final class ProtonDecoder implements Decoder {
+
+    // The decoders for primitives are fixed and cannot be altered by users who want
+    // to register custom decoders.  The decoders created here are stateless and can be
+    // made static to reduce overhead of creating Decoder instances.
+    private static final PrimitiveTypeDecoder<?>[] primitiveDecoders = new PrimitiveTypeDecoder[256];
+
+    static {
+        primitiveDecoders[EncodingCodes.BOOLEAN & 0xFF] = new BooleanTypeDecoder();
+        primitiveDecoders[EncodingCodes.BOOLEAN_TRUE & 0xFF] = new BooleanTrueTypeDecoder();
+        primitiveDecoders[EncodingCodes.BOOLEAN_FALSE & 0xFF] = new BooleanFalseTypeDecoder();
+        primitiveDecoders[EncodingCodes.VBIN8 & 0xFF] = new Binary8TypeDecoder();
+        primitiveDecoders[EncodingCodes.VBIN32 & 0xFF] = new Binary32TypeDecoder();
+        primitiveDecoders[EncodingCodes.BYTE & 0xFF] = new ByteTypeDecoder();
+        primitiveDecoders[EncodingCodes.CHAR & 0xFF] = new CharacterTypeDecoder();
+        primitiveDecoders[EncodingCodes.DECIMAL32 & 0xFF] = new Decimal32TypeDecoder();
+        primitiveDecoders[EncodingCodes.DECIMAL64 & 0xFF] = new Decimal64TypeDecoder();
+        primitiveDecoders[EncodingCodes.DECIMAL128 & 0xFF] = new Decimal128TypeDecoder();
+        primitiveDecoders[EncodingCodes.DOUBLE & 0xFF] = new DoubleTypeDecoder();
+        primitiveDecoders[EncodingCodes.FLOAT & 0xFF] = new FloatTypeDecoder();
+        primitiveDecoders[EncodingCodes.NULL & 0xFF] = new NullTypeDecoder();
+        primitiveDecoders[EncodingCodes.SHORT & 0xFF] = new ShortTypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLINT & 0xFF] = new Integer8TypeDecoder();
+        primitiveDecoders[EncodingCodes.INT & 0xFF] = new Integer32TypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLLONG & 0xFF] = new Long8TypeDecoder();
+        primitiveDecoders[EncodingCodes.LONG & 0xFF] = new LongTypeDecoder();
+        primitiveDecoders[EncodingCodes.UBYTE & 0xFF] = new UnsignedByteTypeDecoder();
+        primitiveDecoders[EncodingCodes.USHORT & 0xFF] = new UnsignedShortTypeDecoder();
+        primitiveDecoders[EncodingCodes.UINT0 & 0xFF] = new UnsignedInteger0TypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLUINT & 0xFF] = new UnsignedInteger8TypeDecoder();
+        primitiveDecoders[EncodingCodes.UINT & 0xFF] = new UnsignedInteger32TypeDecoder();
+        primitiveDecoders[EncodingCodes.ULONG0 & 0xFF] = new UnsignedLong0TypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLULONG & 0xFF] = new UnsignedLong8TypeDecoder();
+        primitiveDecoders[EncodingCodes.ULONG & 0xFF] = new UnsignedLong64TypeDecoder();
+        primitiveDecoders[EncodingCodes.STR8 & 0xFF] = new String8TypeDecoder();
+        primitiveDecoders[EncodingCodes.STR32 & 0xFF] = new String32TypeDecoder();
+        primitiveDecoders[EncodingCodes.SYM8 & 0xFF] = new Symbol8TypeDecoder();
+        primitiveDecoders[EncodingCodes.SYM32 & 0xFF] = new Symbol32TypeDecoder();
+        primitiveDecoders[EncodingCodes.UUID & 0xFF] = new UUIDTypeDecoder();
+        primitiveDecoders[EncodingCodes.TIMESTAMP & 0xFF] = new TimestampTypeDecoder();
+        primitiveDecoders[EncodingCodes.LIST0 & 0xFF] = new List0TypeDecoder();
+        primitiveDecoders[EncodingCodes.LIST8 & 0xFF] = new List8TypeDecoder();
+        primitiveDecoders[EncodingCodes.LIST32 & 0xFF] = new List32TypeDecoder();
+        primitiveDecoders[EncodingCodes.MAP8 & 0xFF] = new Map8TypeDecoder();
+        primitiveDecoders[EncodingCodes.MAP32 & 0xFF] = new Map32TypeDecoder();
+        primitiveDecoders[EncodingCodes.ARRAY8 & 0xFF] = new Array8TypeDecoder();
+        primitiveDecoders[EncodingCodes.ARRAY32 & 0xFF] = new Array32TypeDecoder();
+
+        // Initialize the locally used primitive type decoders for the main API
+        symbol8Decoder = (Symbol8TypeDecoder) primitiveDecoders[EncodingCodes.SYM8 & 0xFF];
+        symbol32Decoder = (Symbol32TypeDecoder) primitiveDecoders[EncodingCodes.SYM32 & 0xFF];
+        binary8Decoder = (Binary8TypeDecoder) primitiveDecoders[EncodingCodes.VBIN8 & 0xFF];
+        binary32Decoder = (Binary32TypeDecoder) primitiveDecoders[EncodingCodes.VBIN32 & 0xFF];
+        list8Decoder = (List8TypeDecoder) primitiveDecoders[EncodingCodes.LIST8 & 0xFF];
+        list32Decoder = (List32TypeDecoder) primitiveDecoders[EncodingCodes.LIST32 & 0xFF];
+        map8Decoder = (Map8TypeDecoder) primitiveDecoders[EncodingCodes.MAP8 & 0xFF];
+        map32Decoder = (Map32TypeDecoder) primitiveDecoders[EncodingCodes.MAP32 & 0xFF];
+        string32Decoder = (String32TypeDecoder) primitiveDecoders[EncodingCodes.STR32 & 0xFF];
+        string8Decoder = (String8TypeDecoder) primitiveDecoders[EncodingCodes.STR8 & 0xFF];
+    }
+
+    // Registry of decoders for described types which can be updated with user defined
+    // decoders as well as the default decoders.
+    private Map<Object, DescribedTypeDecoder<?>> describedTypeDecoders = new HashMap<>();
+
+    // Quick access to decoders that handle AMQP types like Transfer, Properties etc.
+    private final DescribedTypeDecoder<?>[] amqpTypeDecoders = new DescribedTypeDecoder[256];
+
+    private ProtonDecoderState singleThreadedState;
+
+    // Internal Decoders used to prevent user to access Proton specific decoding methods
+    private static final Symbol8TypeDecoder symbol8Decoder;
+    private static final Symbol32TypeDecoder symbol32Decoder;
+    private static final Binary8TypeDecoder binary8Decoder;
+    private static final Binary32TypeDecoder binary32Decoder;
+    private static final List8TypeDecoder list8Decoder;
+    private static final List32TypeDecoder list32Decoder;
+    private static final Map8TypeDecoder map8Decoder;
+    private static final Map32TypeDecoder map32Decoder;
+    private static final String8TypeDecoder string8Decoder;
+    private static final String32TypeDecoder string32Decoder;
+
+    @Override
+    public ProtonDecoderState newDecoderState() {
+        return new ProtonDecoderState(this);
+    }
+
+    @Override
+    public ProtonDecoderState getCachedDecoderState() {
+        ProtonDecoderState state = singleThreadedState;
+        if (state == null) {
+            singleThreadedState = state = newDecoderState();
+        }
+
+        return state.reset();
+    }
+
+    @Override
+    public Object readObject(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        TypeDecoder<?> decoder = readNextTypeDecoder(buffer, state);
+
+        if (decoder == null) {
+            throw new DecodeException("Unknown type constructor in encoded bytes");
+        }
+
+        return decoder.readValue(buffer, state);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T readObject(ProtonBuffer buffer, DecoderState state, final Class<T> clazz) throws DecodeException {
+        Object result = readObject(buffer, state);
+
+        if (result == null) {
+            return null;
+        } else if (clazz.isAssignableFrom(result.getClass())) {
+            return (T) result;
+        } else {
+            throw signalUnexpectedType(result, clazz);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T[] readMultiple(ProtonBuffer buffer, DecoderState state, final Class<T> clazz) throws DecodeException {
+        Object val = readObject(buffer, state);
+
+        if (val == null) {
+            return null;
+        } else if (val.getClass().isArray()) {
+            if (clazz.isAssignableFrom(val.getClass().getComponentType())) {
+                return (T[]) val;
+            } else {
+                throw signalUnexpectedType(val, Array.newInstance(clazz, 0).getClass());
+            }
+        } else if (clazz.isAssignableFrom(val.getClass())) {
+            T[] array = (T[]) Array.newInstance(clazz, 1);
+            array[0] = (T) val;
+            return array;
+        } else {
+            throw signalUnexpectedType(val, Array.newInstance(clazz, 0).getClass());
+        }
+    }
+
+    @Override
+    public TypeDecoder<?> readNextTypeDecoder(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int encodingCode = readEncodingCode(buffer) & 0xff;
+
+        if (encodingCode == EncodingCodes.DESCRIBED_TYPE_INDICATOR) {
+            buffer.markReadIndex();
+            try {
+                final long result = readUnsignedLong(buffer, state, amqpTypeDecoders.length);
+
+                if (result > 0 && result < amqpTypeDecoders.length && amqpTypeDecoders[(int) result] != null) {
+                    return amqpTypeDecoders[(int) result];
+                } else {
+                    buffer.resetReadIndex();
+                    return slowReadNextTypeDecoder(buffer, state);
+                }
+            } catch (Exception e) {
+                buffer.resetReadIndex();
+                return slowReadNextTypeDecoder(buffer, state);
+            }
+        } else {
+            return primitiveDecoders[encodingCode];
+        }
+    }
+
+    private TypeDecoder<?> slowReadNextTypeDecoder(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        Object descriptor;
+        buffer.markReadIndex();
+        try {
+            descriptor = readUnsignedLong(buffer, state);
+        } catch (Exception e) {
+            buffer.resetReadIndex();
+            descriptor = readObject(buffer, state);
+        }
+
+        TypeDecoder<?> typeDecoder = describedTypeDecoders.get(descriptor);
+        if (typeDecoder == null) {
+            typeDecoder = handleUnknownDescribedType(descriptor);
+        }
+
+        return typeDecoder;
+    }
+
+    @Override
+    public TypeDecoder<?> peekNextTypeDecoder(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.markReadIndex();
+        try {
+            return readNextTypeDecoder(buffer, state);
+        } finally {
+            buffer.resetReadIndex();
+        }
+    }
+
+    @Override
+    public <V> ProtonDecoder registerDescribedTypeDecoder(DescribedTypeDecoder<V> decoder) {
+        DescribedTypeDecoder<?> describedTypeDecoder = decoder;
+
+        // Cache AMQP type decoders in the quick lookup array.
+        if (decoder.getDescriptorCode().compareTo(amqpTypeDecoders.length) < 0) {
+            amqpTypeDecoders[decoder.getDescriptorCode().intValue()] = decoder;
+        }
+
+        describedTypeDecoders.put(describedTypeDecoder.getDescriptorCode(), describedTypeDecoder);
+        describedTypeDecoders.put(describedTypeDecoder.getDescriptorSymbol(), describedTypeDecoder);
+
+        return this;
+    }
+
+    @Override
+    public Boolean readBoolean(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.BOOLEAN_TRUE:
+                return Boolean.TRUE;
+            case EncodingCodes.BOOLEAN_FALSE:
+                return Boolean.FALSE;
+            case EncodingCodes.BOOLEAN:
+                return buffer.readByte() == 0 ? Boolean.FALSE : Boolean.TRUE;
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Boolean type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public boolean readBoolean(ProtonBuffer buffer, DecoderState state, boolean defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.BOOLEAN_TRUE:
+                return true;
+            case EncodingCodes.BOOLEAN_FALSE:
+                return false;
+            case EncodingCodes.BOOLEAN:
+                return buffer.readByte() == 0 ? false : true;
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Boolean type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Byte readByte(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.BYTE:
+                return buffer.readByte();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public byte readByte(ProtonBuffer buffer, DecoderState state, byte defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.BYTE:
+                return buffer.readByte();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedByte readUnsignedByte(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.UBYTE:
+                return UnsignedByte.valueOf(buffer.readByte());
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public byte readUnsignedByte(ProtonBuffer buffer, DecoderState state, byte defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.UBYTE:
+                return buffer.readByte();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Character readCharacter(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.CHAR:
+                return Character.valueOf((char) (buffer.readInt() & 0xffff));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Character type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public char readCharacter(ProtonBuffer buffer, DecoderState state, char defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.CHAR:
+                return (char) (buffer.readInt() & 0xffff);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Character type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Decimal32 readDecimal32(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.DECIMAL32:
+                return new Decimal32(buffer.readInt());
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Decimal32 type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Decimal64 readDecimal64(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.DECIMAL64:
+                return new Decimal64(buffer.readLong());
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Decimal64 type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Decimal128 readDecimal128(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.DECIMAL128:
+                return new Decimal128(buffer.readLong(), buffer.readLong());
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Decimal128 type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Short readShort(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SHORT:
+                return buffer.readShort();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public short readShort(ProtonBuffer buffer, DecoderState state, short defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SHORT:
+                return buffer.readShort();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedShort readUnsignedShort(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.USHORT:
+                return UnsignedShort.valueOf(buffer.readShort());
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public short readUnsignedShort(ProtonBuffer buffer, DecoderState state, short defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.USHORT:
+                return buffer.readShort();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public int readUnsignedShort(ProtonBuffer buffer, DecoderState state, int defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.USHORT:
+                return buffer.readShort() & 0xffff;
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Integer readInteger(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLINT:
+                return buffer.readByte() & 0xff;
+            case EncodingCodes.INT:
+                return buffer.readInt();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public int readInteger(ProtonBuffer buffer, DecoderState state, int defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLINT:
+                return buffer.readByte() & 0xff;
+            case EncodingCodes.INT:
+                return buffer.readInt();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedInteger readUnsignedInteger(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.UINT0:
+                return UnsignedInteger.ZERO;
+            case EncodingCodes.SMALLUINT:
+                return UnsignedInteger.valueOf((buffer.readByte()) & 0xff);
+            case EncodingCodes.UINT:
+                return UnsignedInteger.valueOf((buffer.readInt()));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public int readUnsignedInteger(ProtonBuffer buffer, DecoderState state, int defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.UINT0:
+                return 0;
+            case EncodingCodes.SMALLUINT:
+                return buffer.readByte() & 0xff;
+            case EncodingCodes.UINT:
+                return buffer.readInt();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readUnsignedInteger(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.UINT0:
+                return 0;
+            case EncodingCodes.SMALLUINT:
+                return buffer.readByte() & 0xff;
+            case EncodingCodes.UINT:
+                return buffer.readInt() & 0xffffffffl;
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Long readLong(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLLONG:
+                return (long) buffer.readByte() & 0xff;
+            case EncodingCodes.LONG:
+                return buffer.readLong();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readLong(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLLONG:
+                return (long) buffer.readByte() & 0xff;
+            case EncodingCodes.LONG:
+                return buffer.readLong();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedLong readUnsignedLong(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.ULONG0:
+                return UnsignedLong.ZERO;
+            case EncodingCodes.SMALLULONG:
+                return UnsignedLong.valueOf((buffer.readByte() & 0xff));
+            case EncodingCodes.ULONG:
+                return UnsignedLong.valueOf((buffer.readLong()));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readUnsignedLong(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.ULONG0:
+                return 0l;
+            case EncodingCodes.SMALLULONG:
+                return (buffer.readByte() & 0xff);
+            case EncodingCodes.ULONG:
+                return buffer.readLong();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Float readFloat(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.FLOAT:
+                return buffer.readFloat();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Float type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public float readFloat(ProtonBuffer buffer, DecoderState state, float defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.FLOAT:
+                return buffer.readFloat();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Float type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Double readDouble(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.DOUBLE:
+                return buffer.readDouble();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Double type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public double readDouble(ProtonBuffer buffer, DecoderState state, double defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.DOUBLE:
+                return buffer.readDouble();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Double type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Binary readBinary(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                return binary8Decoder.readValue(buffer, state);
+            case EncodingCodes.VBIN32:
+                return binary32Decoder.readValue(buffer, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public ProtonBuffer readBinaryAsBuffer(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                return binary8Decoder.readValueAsBuffer(buffer, state);
+            case EncodingCodes.VBIN32:
+                return binary32Decoder.readValueAsBuffer(buffer, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public DeliveryTag readDeliveryTag(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                return new DeliveryTag.ProtonDeliveryTag(binary8Decoder.readValueAsArray(buffer, state));
+            case EncodingCodes.VBIN32:
+                return new DeliveryTag.ProtonDeliveryTag(binary32Decoder.readValueAsArray(buffer, state));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public String readString(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.STR8:
+                return string8Decoder.readValue(buffer, state);
+            case EncodingCodes.STR32:
+                return string32Decoder.readValue(buffer, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected String type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Symbol readSymbol(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SYM8:
+                return symbol8Decoder.readValue(buffer, state);
+            case EncodingCodes.SYM32:
+                return symbol32Decoder.readValue(buffer, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Symbol type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public String readSymbol(ProtonBuffer buffer, DecoderState state, String defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.SYM8:
+                return symbol8Decoder.readString(buffer, state);
+            case EncodingCodes.SYM32:
+                return symbol32Decoder.readString(buffer, state);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Symbol type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Long readTimestamp(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.TIMESTAMP:
+                return buffer.readLong();
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Timestamp type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readTimestamp(ProtonBuffer buffer, DecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.TIMESTAMP:
+                return buffer.readLong();
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Timestamp type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UUID readUUID(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.UUID:
+                return new UUID(buffer.readLong(), buffer.readLong());
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected UUID type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <K, V> Map<K, V> readMap(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.MAP8:
+                return (Map<K, V>) map8Decoder.readValue(buffer, state);
+            case EncodingCodes.MAP32:
+                return (Map<K, V>) map32Decoder.readValue(buffer, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Map type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <V> List<V> readList(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = readEncodingCode(buffer);
+
+        switch (encodingCode) {
+            case EncodingCodes.LIST0:
+                return Collections.emptyList();
+            case EncodingCodes.LIST8:
+                return (List<V>) list8Decoder.readValue(buffer, state);
+            case EncodingCodes.LIST32:
+                return (List<V>) list32Decoder.readValue(buffer, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected List type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    private static byte readEncodingCode(ProtonBuffer buffer) throws DecodeEOFException {
+        try {
+            return buffer.readByte();
+        } catch (IndexOutOfBoundsException iobe) {
+            throw new DecodeEOFException("Read of new type failed because buffer exhausted.", iobe);
+        }
+    }
+
+    private ClassCastException signalUnexpectedType(final Object val, Class<?> clazz) {
+        return new ClassCastException("Unexpected type " + val.getClass().getName() +
+                                      ". Expected " + clazz.getName() + ".");
+    }
+
+    private TypeDecoder<?> handleUnknownDescribedType(final Object descriptor) {
+        TypeDecoder<?> typeDecoder = new UnknownDescribedTypeDecoder() {
+
+            @Override
+            public Object getDescriptor() {
+                return descriptor;
+            }
+        };
+
+        describedTypeDecoders.put(descriptor, (UnknownDescribedTypeDecoder) typeDecoder);
+
+        return typeDecoder;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderFactory.java
new file mode 100644
index 0000000..1da05e7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderFactory.java
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import org.apache.qpid.protonj2.codec.decoders.messaging.AcceptedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AmqpSequenceTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AmqpValueTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ApplicationPropertiesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DataTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnCloseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoLinksOrMessagesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoLinksTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoMessagesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeliveryAnnotationsTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.FooterTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.HeaderTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.MessageAnnotationsTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ModifiedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.PropertiesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ReceivedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.RejectedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ReleasedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.SourceTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.TargetTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslChallengeTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslInitTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslMechanismsTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslOutcomeTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslResponseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.CoordinatorTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DeclareTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DeclaredTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DischargeTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.TransactionStateTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.AttachTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.BeginTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.CloseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.DetachTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.DispositionTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.EndTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.ErrorConditionTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.FlowTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.OpenTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.TransferTypeDecoder;
+
+/**
+ * Factory that create and initializes new BuiltinDecoder instances
+ */
+public final class ProtonDecoderFactory {
+
+    private ProtonDecoderFactory() {
+    }
+
+    public static ProtonDecoder create() {
+        ProtonDecoder decoder = new ProtonDecoder();
+
+        addMessagingTypeDecoders(decoder);
+        addTransactionTypeDecoders(decoder);
+        addTransportTypeDecoders(decoder);
+
+        return decoder;
+    }
+
+    public static ProtonDecoder createSasl() {
+        ProtonDecoder decoder = new ProtonDecoder();
+
+        addSaslTypeDecoders(decoder);
+
+        return decoder;
+    }
+
+    private static void addMessagingTypeDecoders(ProtonDecoder Decoder) {
+        Decoder.registerDescribedTypeDecoder(new AcceptedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new AmqpSequenceTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new AmqpValueTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ApplicationPropertiesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DataTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnCloseTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnNoLinksOrMessagesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnNoLinksTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnNoMessagesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeliveryAnnotationsTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new FooterTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new HeaderTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new MessageAnnotationsTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ModifiedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new PropertiesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ReceivedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new RejectedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ReleasedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new SourceTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new TargetTypeDecoder());
+    }
+
+    private static void addTransactionTypeDecoders(ProtonDecoder Decoder) {
+        Decoder.registerDescribedTypeDecoder(new CoordinatorTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeclaredTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeclareTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DischargeTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new TransactionStateTypeDecoder());
+    }
+
+    private static void addTransportTypeDecoders(ProtonDecoder Decoder) {
+        Decoder.registerDescribedTypeDecoder(new AttachTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new BeginTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new CloseTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DetachTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DispositionTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new EndTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ErrorConditionTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new FlowTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new OpenTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new TransferTypeDecoder());
+    }
+
+    private static void addSaslTypeDecoders(ProtonDecoder decoder) {
+        decoder.registerDescribedTypeDecoder(new SaslChallengeTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslInitTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslMechanismsTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslOutcomeTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslResponseTypeDecoder());
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderState.java
new file mode 100644
index 0000000..08f57b5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderState.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+
+/**
+ * State object used by the Built in Decoder implementation.
+ */
+public final class ProtonDecoderState implements DecoderState {
+
+    private static final int MAX_CHAR_BUFFER_CAHCE_SIZE = 100;
+
+    private final CharsetDecoder STRING_DECODER = StandardCharsets.UTF_8.newDecoder();
+    private final ProtonDecoder decoder;
+    private final char[] decodeCache = new char[MAX_CHAR_BUFFER_CAHCE_SIZE];
+
+    private UTF8Decoder stringDecoder;
+
+    public ProtonDecoderState(ProtonDecoder decoder) {
+        this.decoder = decoder;
+    }
+
+    @Override
+    public ProtonDecoder getDecoder() {
+        return decoder;
+    }
+
+    @Override
+    public ProtonDecoderState reset() {
+        // No intermediate state to reset
+        return this;
+    }
+
+    public UTF8Decoder getStringDecoder() {
+        return stringDecoder;
+    }
+
+    public void setStringDecoder(UTF8Decoder stringDecoder) {
+        this.stringDecoder = stringDecoder;
+    }
+
+    @Override
+    public String decodeUTF8(ProtonBuffer buffer, int length) throws DecodeException {
+        if (stringDecoder == null) {
+            return internalDecode(buffer, length, STRING_DECODER, length > MAX_CHAR_BUFFER_CAHCE_SIZE ? new char[length] : decodeCache);
+        } else {
+            final int originalPosition = buffer.getReadIndex();
+
+            try {
+                return stringDecoder.decodeUTF8(buffer, length);
+            } catch (Exception ex) {
+                throw new DecodeException("Cannot parse encoded UTF8 String", ex);
+            } finally {
+                buffer.setReadIndex(originalPosition + length);
+            }
+        }
+    }
+
+    private static String internalDecode(ProtonBuffer buffer, final int length, CharsetDecoder decoder, char[] scratch) {
+        final int bufferInitialPosition = buffer.getReadIndex();
+
+        int offset;
+        for (offset = 0; offset < length; offset++) {
+            final byte b = buffer.getByte(bufferInitialPosition + offset);
+            if (b < 0) {
+                break;
+            }
+            scratch[offset] = (char) b;
+        }
+
+        buffer.setReadIndex(bufferInitialPosition + offset);
+
+        if (offset == length) {
+            return new String(scratch, 0, length);
+        } else {
+            return internalDecodeUTF8(buffer, length, scratch, offset, decoder);
+        }
+    }
+
+    private static String internalDecodeUTF8(final ProtonBuffer buffer, final int length, final char[] chars, final int offset, final CharsetDecoder decoder) {
+        final CharBuffer out = CharBuffer.wrap(chars);
+        out.position(offset);
+
+        // Create a buffer from the remaining portion of the buffer and then use the decoder to complete the work
+        // remember to move the main buffer position to consume the data processed.
+        ProtonBuffer slice = buffer.slice(buffer.getReadIndex(), length - offset);
+        buffer.setReadIndex(buffer.getReadIndex() + slice.getReadableBytes());
+        ByteBuffer byteBuffer = slice.toByteBuffer();
+
+        try {
+            for (;;) {
+                CoderResult cr = byteBuffer.hasRemaining() ? decoder.decode(byteBuffer, out, true) : CoderResult.UNDERFLOW;
+                if (cr.isUnderflow()) {
+                    cr = decoder.flush(out);
+                }
+                if (cr.isUnderflow()) {
+                    break;
+                }
+
+                // The char buffer should have been sufficient here but wasn't so we know
+                // that there was some encoding issue on the other end.
+                cr.throwException();
+            }
+
+            return out.flip().toString();
+        } catch (CharacterCodingException e) {
+            throw new DecodeException("Cannot parse encoded UTF8 String", e);
+        } finally {
+            decoder.reset();
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoder.java
new file mode 100644
index 0000000..f345142
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoder.java
@@ -0,0 +1,958 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoder;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Array32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Array8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Binary32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Binary8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BooleanFalseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BooleanTrueTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BooleanTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ByteTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.CharacterTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal128TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal64TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.DoubleTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.FloatTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Integer32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Integer8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.List0TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.List32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.List8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Long8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.LongTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Map32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Map8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ShortTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.String32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.String8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Symbol32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Symbol8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.TimestampTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UUIDTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedByteTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedInteger0TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedInteger32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedInteger8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedLong0TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedLong64TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedLong8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.UnsignedShortTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * The default AMQP Decoder implementation.
+ */
+public final class ProtonStreamDecoder implements StreamDecoder {
+
+    private static final int STREAM_PEEK_MARK_LIMIT = 64;
+
+    // The decoders for primitives are fixed and cannot be altered by users who want
+    // to register custom decoders.  The decoders created here are stateless and can be
+    // made static to reduce overhead of creating Decoder instances.
+    private static final PrimitiveTypeDecoder<?>[] primitiveDecoders = new PrimitiveTypeDecoder[256];
+
+    static {
+        primitiveDecoders[EncodingCodes.BOOLEAN & 0xFF] = new BooleanTypeDecoder();
+        primitiveDecoders[EncodingCodes.BOOLEAN_TRUE & 0xFF] = new BooleanTrueTypeDecoder();
+        primitiveDecoders[EncodingCodes.BOOLEAN_FALSE & 0xFF] = new BooleanFalseTypeDecoder();
+        primitiveDecoders[EncodingCodes.VBIN8 & 0xFF] = new Binary8TypeDecoder();
+        primitiveDecoders[EncodingCodes.VBIN32 & 0xFF] = new Binary32TypeDecoder();
+        primitiveDecoders[EncodingCodes.BYTE & 0xFF] = new ByteTypeDecoder();
+        primitiveDecoders[EncodingCodes.CHAR & 0xFF] = new CharacterTypeDecoder();
+        primitiveDecoders[EncodingCodes.DECIMAL32 & 0xFF] = new Decimal32TypeDecoder();
+        primitiveDecoders[EncodingCodes.DECIMAL64 & 0xFF] = new Decimal64TypeDecoder();
+        primitiveDecoders[EncodingCodes.DECIMAL128 & 0xFF] = new Decimal128TypeDecoder();
+        primitiveDecoders[EncodingCodes.DOUBLE & 0xFF] = new DoubleTypeDecoder();
+        primitiveDecoders[EncodingCodes.FLOAT & 0xFF] = new FloatTypeDecoder();
+        primitiveDecoders[EncodingCodes.NULL & 0xFF] = new NullTypeDecoder();
+        primitiveDecoders[EncodingCodes.SHORT & 0xFF] = new ShortTypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLINT & 0xFF] = new Integer8TypeDecoder();
+        primitiveDecoders[EncodingCodes.INT & 0xFF] = new Integer32TypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLLONG & 0xFF] = new Long8TypeDecoder();
+        primitiveDecoders[EncodingCodes.LONG & 0xFF] = new LongTypeDecoder();
+        primitiveDecoders[EncodingCodes.UBYTE & 0xFF] = new UnsignedByteTypeDecoder();
+        primitiveDecoders[EncodingCodes.USHORT & 0xFF] = new UnsignedShortTypeDecoder();
+        primitiveDecoders[EncodingCodes.UINT0 & 0xFF] = new UnsignedInteger0TypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLUINT & 0xFF] = new UnsignedInteger8TypeDecoder();
+        primitiveDecoders[EncodingCodes.UINT & 0xFF] = new UnsignedInteger32TypeDecoder();
+        primitiveDecoders[EncodingCodes.ULONG0 & 0xFF] = new UnsignedLong0TypeDecoder();
+        primitiveDecoders[EncodingCodes.SMALLULONG & 0xFF] = new UnsignedLong8TypeDecoder();
+        primitiveDecoders[EncodingCodes.ULONG & 0xFF] = new UnsignedLong64TypeDecoder();
+        primitiveDecoders[EncodingCodes.STR8 & 0xFF] = new String8TypeDecoder();
+        primitiveDecoders[EncodingCodes.STR32 & 0xFF] = new String32TypeDecoder();
+        primitiveDecoders[EncodingCodes.SYM8 & 0xFF] = new Symbol8TypeDecoder();
+        primitiveDecoders[EncodingCodes.SYM32 & 0xFF] = new Symbol32TypeDecoder();
+        primitiveDecoders[EncodingCodes.UUID & 0xFF] = new UUIDTypeDecoder();
+        primitiveDecoders[EncodingCodes.TIMESTAMP & 0xFF] = new TimestampTypeDecoder();
+        primitiveDecoders[EncodingCodes.LIST0 & 0xFF] = new List0TypeDecoder();
+        primitiveDecoders[EncodingCodes.LIST8 & 0xFF] = new List8TypeDecoder();
+        primitiveDecoders[EncodingCodes.LIST32 & 0xFF] = new List32TypeDecoder();
+        primitiveDecoders[EncodingCodes.MAP8 & 0xFF] = new Map8TypeDecoder();
+        primitiveDecoders[EncodingCodes.MAP32 & 0xFF] = new Map32TypeDecoder();
+        primitiveDecoders[EncodingCodes.ARRAY8 & 0xFF] = new Array8TypeDecoder();
+        primitiveDecoders[EncodingCodes.ARRAY32 & 0xFF] = new Array32TypeDecoder();
+
+        // Initialize the locally used primitive type decoders for the main API
+        symbol8Decoder = (Symbol8TypeDecoder) primitiveDecoders[EncodingCodes.SYM8 & 0xFF];
+        symbol32Decoder = (Symbol32TypeDecoder) primitiveDecoders[EncodingCodes.SYM32 & 0xFF];
+        binary8Decoder = (Binary8TypeDecoder) primitiveDecoders[EncodingCodes.VBIN8 & 0xFF];
+        binary32Decoder = (Binary32TypeDecoder) primitiveDecoders[EncodingCodes.VBIN32 & 0xFF];
+        list8Decoder = (List8TypeDecoder) primitiveDecoders[EncodingCodes.LIST8 & 0xFF];
+        list32Decoder = (List32TypeDecoder) primitiveDecoders[EncodingCodes.LIST32 & 0xFF];
+        map8Decoder = (Map8TypeDecoder) primitiveDecoders[EncodingCodes.MAP8 & 0xFF];
+        map32Decoder = (Map32TypeDecoder) primitiveDecoders[EncodingCodes.MAP32 & 0xFF];
+        string32Decoder = (String32TypeDecoder) primitiveDecoders[EncodingCodes.STR32 & 0xFF];
+        string8Decoder = (String8TypeDecoder) primitiveDecoders[EncodingCodes.STR8 & 0xFF];
+    }
+
+    // Registry of decoders for described types which can be updated with user defined
+    // decoders as well as the default decoders.
+    private Map<Object, StreamDescribedTypeDecoder<?>> describedTypeDecoders = new HashMap<>();
+
+    // Quick access to decoders that handle AMQP types like Transfer, Properties etc.
+    private final StreamDescribedTypeDecoder<?>[] amqpTypeDecoders = new StreamDescribedTypeDecoder[256];
+
+    private ProtonStreamDecoderState singleThreadedState;
+
+    // Internal Decoders used to prevent user to access Proton specific decoding methods
+    private static final Symbol8TypeDecoder symbol8Decoder;
+    private static final Symbol32TypeDecoder symbol32Decoder;
+    private static final Binary8TypeDecoder binary8Decoder;
+    private static final Binary32TypeDecoder binary32Decoder;
+    private static final List8TypeDecoder list8Decoder;
+    private static final List32TypeDecoder list32Decoder;
+    private static final Map8TypeDecoder map8Decoder;
+    private static final Map32TypeDecoder map32Decoder;
+    private static final String8TypeDecoder string8Decoder;
+    private static final String32TypeDecoder string32Decoder;
+
+    @Override
+    public ProtonStreamDecoderState newDecoderState() {
+        return new ProtonStreamDecoderState(this);
+    }
+
+    @Override
+    public ProtonStreamDecoderState getCachedDecoderState() {
+        ProtonStreamDecoderState state = singleThreadedState;
+        if (state == null) {
+            singleThreadedState = state = newDecoderState();
+        }
+
+        return state.reset();
+    }
+
+    @Override
+    public Object readObject(InputStream stream, StreamDecoderState state) throws DecodeException {
+        StreamTypeDecoder<?> decoder = readNextTypeDecoder(stream, state);
+
+        if (decoder == null) {
+            throw new DecodeException("Unknown type constructor in encoded bytes");
+        }
+
+        return decoder.readValue(stream, state);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T readObject(InputStream stream, StreamDecoderState state, final Class<T> clazz) throws DecodeException {
+        Object result = readObject(stream, state);
+
+        if (result == null) {
+            return null;
+        } else if (clazz.isAssignableFrom(result.getClass())) {
+            return (T) result;
+        } else {
+            throw signalUnexpectedType(result, clazz);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T[] readMultiple(InputStream stream, StreamDecoderState state, final Class<T> clazz) throws DecodeException {
+        Object val = readObject(stream, state);
+
+        if (val == null) {
+            return null;
+        } else if (val.getClass().isArray()) {
+            if (clazz.isAssignableFrom(val.getClass().getComponentType())) {
+                return (T[]) val;
+            } else {
+                throw signalUnexpectedType(val, Array.newInstance(clazz, 0).getClass());
+            }
+        } else if (clazz.isAssignableFrom(val.getClass())) {
+            T[] array = (T[]) Array.newInstance(clazz, 1);
+            array[0] = (T) val;
+            return array;
+        } else {
+            throw signalUnexpectedType(val, Array.newInstance(clazz, 0).getClass());
+        }
+    }
+
+    @Override
+    public StreamTypeDecoder<?> readNextTypeDecoder(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        if (encodingCode == EncodingCodes.DESCRIBED_TYPE_INDICATOR) {
+            if (stream.markSupported()) {
+                stream.mark(STREAM_PEEK_MARK_LIMIT);
+                try {
+                    final long result = readUnsignedLong(stream, state, amqpTypeDecoders.length);
+
+                    if (result > 0 && result < amqpTypeDecoders.length && amqpTypeDecoders[(int) result] != null) {
+                        return amqpTypeDecoders[(int) result];
+                    } else {
+                        ProtonStreamUtils.reset(stream);
+                        return slowReadNextTypeDecoder(stream, state);
+                    }
+                } catch (Exception e) {
+                    ProtonStreamUtils.reset(stream);
+                    return slowReadNextTypeDecoder(stream, state);
+                }
+            } else {
+                return slowReadNextTypeDecoder(stream, state);
+            }
+        } else {
+            return primitiveDecoders[encodingCode & 0xff];
+        }
+    }
+
+    private StreamTypeDecoder<?> slowReadNextTypeDecoder(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+        final Object descriptor;
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLULONG:
+                descriptor = UnsignedLong.valueOf(ProtonStreamUtils.readByte(stream) & 0xffl);
+                break;
+            case EncodingCodes.ULONG:
+                descriptor = UnsignedLong.valueOf(ProtonStreamUtils.readLong(stream));
+                break;
+            case EncodingCodes.SYM8:
+                descriptor = symbol8Decoder.readValue(stream, state);
+                break;
+            case EncodingCodes.SYM32:
+                descriptor = symbol32Decoder.readValue(stream, state);
+                break;
+            default:
+                throw new DecodeException("Expected Descriptor type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+
+        StreamTypeDecoder<?> StreamTypeDecoder = describedTypeDecoders.get(descriptor);
+        if (StreamTypeDecoder == null) {
+            StreamTypeDecoder = handleUnknownDescribedType(descriptor);
+        }
+
+        return StreamTypeDecoder;
+    }
+
+    @Override
+    public StreamTypeDecoder<?> peekNextTypeDecoder(InputStream stream, StreamDecoderState state) throws DecodeException {
+        if (stream.markSupported()) {
+            stream.mark(STREAM_PEEK_MARK_LIMIT);
+            try {
+                return readNextTypeDecoder(stream, state);
+            } finally {
+                try {
+                    stream.reset();
+                } catch (IOException e) {
+                    throw new DecodeException("Error while reseting marked stream", e);
+                }
+            }
+        } else {
+            throw new UnsupportedOperationException("The provided stream doesn't support stream marks");
+        }
+    }
+
+    @Override
+    public <V> ProtonStreamDecoder registerDescribedTypeDecoder(StreamDescribedTypeDecoder<V> decoder) {
+        StreamDescribedTypeDecoder<?> describedTypeDecoder = decoder;
+
+        // Cache AMQP type decoders in the quick lookup array.
+        if (decoder.getDescriptorCode().compareTo(amqpTypeDecoders.length) < 0) {
+            amqpTypeDecoders[decoder.getDescriptorCode().intValue()] = decoder;
+        }
+
+        describedTypeDecoders.put(describedTypeDecoder.getDescriptorCode(), describedTypeDecoder);
+        describedTypeDecoders.put(describedTypeDecoder.getDescriptorSymbol(), describedTypeDecoder);
+
+        return this;
+    }
+
+    @Override
+    public Boolean readBoolean(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.BOOLEAN_TRUE:
+                return Boolean.TRUE;
+            case EncodingCodes.BOOLEAN_FALSE:
+                return Boolean.FALSE;
+            case EncodingCodes.BOOLEAN:
+                return ProtonStreamUtils.readByte(stream) == 0 ? Boolean.FALSE : Boolean.TRUE;
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Boolean type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public boolean readBoolean(InputStream stream, StreamDecoderState state, boolean defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.BOOLEAN_TRUE:
+                return true;
+            case EncodingCodes.BOOLEAN_FALSE:
+                return false;
+            case EncodingCodes.BOOLEAN:
+                return ProtonStreamUtils.readByte(stream) == 0 ? false : true;
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Boolean type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Byte readByte(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.BYTE:
+                return ProtonStreamUtils.readByte(stream);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public byte readByte(InputStream stream, StreamDecoderState state, byte defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.BYTE:
+                return ProtonStreamUtils.readByte(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedByte readUnsignedByte(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.UBYTE:
+                return UnsignedByte.valueOf(ProtonStreamUtils.readByte(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public byte readUnsignedByte(InputStream stream, StreamDecoderState state, byte defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.UBYTE:
+                return ProtonStreamUtils.readByte(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Byte type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Character readCharacter(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.CHAR:
+                return Character.valueOf((char) (ProtonStreamUtils.readInt(stream) & 0xFFFF));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Character type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public char readCharacter(InputStream stream, StreamDecoderState state, char defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.CHAR:
+                return (char) (ProtonStreamUtils.readInt(stream) & 0xFFFF);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Character type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Decimal32 readDecimal32(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.DECIMAL32:
+                return new Decimal32(ProtonStreamUtils.readInt(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Decimal32 type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Decimal64 readDecimal64(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.DECIMAL64:
+                return new Decimal64(ProtonStreamUtils.readLong(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Decimal64 type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Decimal128 readDecimal128(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.DECIMAL128:
+                return new Decimal128(ProtonStreamUtils.readLong(stream), ProtonStreamUtils.readLong(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Decimal128 type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Short readShort(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SHORT:
+                return ProtonStreamUtils.readShort(stream);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public short readShort(InputStream stream, StreamDecoderState state, short defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SHORT:
+                return ProtonStreamUtils.readShort(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedShort readUnsignedShort(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.USHORT:
+                return UnsignedShort.valueOf(ProtonStreamUtils.readShort(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public short readUnsignedShort(InputStream stream, StreamDecoderState state, short defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.USHORT:
+                return ProtonStreamUtils.readShort(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public int readUnsignedShort(InputStream stream, StreamDecoderState state, int defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.USHORT:
+                return ProtonStreamUtils.readShort(stream) & 0xFFFF;
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Short type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Integer readInteger(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLINT:
+                return (int) ProtonStreamUtils.readByte(stream);
+            case EncodingCodes.INT:
+                return ProtonStreamUtils.readInt(stream);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public int readInteger(InputStream stream, StreamDecoderState state, int defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLINT:
+                return ProtonStreamUtils.readByte(stream);
+            case EncodingCodes.INT:
+                return ProtonStreamUtils.readInt(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedInteger readUnsignedInteger(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.UINT0:
+                return UnsignedInteger.ZERO;
+            case EncodingCodes.SMALLUINT:
+                return UnsignedInteger.valueOf(ProtonStreamUtils.readByte(stream) & 0xff);
+            case EncodingCodes.UINT:
+                return UnsignedInteger.valueOf(ProtonStreamUtils.readInt(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public int readUnsignedInteger(InputStream stream, StreamDecoderState state, int defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.UINT0:
+                return 0;
+            case EncodingCodes.SMALLUINT:
+                return ProtonStreamUtils.readByte(stream) & 0xff;
+            case EncodingCodes.UINT:
+                return ProtonStreamUtils.readInt(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readUnsignedInteger(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.UINT0:
+                return 0;
+            case EncodingCodes.SMALLUINT:
+                return ProtonStreamUtils.readByte(stream) & 0xffl;
+            case EncodingCodes.UINT:
+                return ProtonStreamUtils.readInt(stream) & 0xffffffffl;
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Integer type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Long readLong(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLLONG:
+                return Long.valueOf(ProtonStreamUtils.readByte(stream) & 0xffl);
+            case EncodingCodes.LONG:
+                return ProtonStreamUtils.readLong(stream);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readLong(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SMALLLONG:
+                return ProtonStreamUtils.readByte(stream) & 0xffl;
+            case EncodingCodes.LONG:
+                return ProtonStreamUtils.readLong(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UnsignedLong readUnsignedLong(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.ULONG0:
+                return UnsignedLong.ZERO;
+            case EncodingCodes.SMALLULONG:
+                return UnsignedLong.valueOf(ProtonStreamUtils.readByte(stream) & 0xffl);
+            case EncodingCodes.ULONG:
+                return UnsignedLong.valueOf(ProtonStreamUtils.readLong(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Unsigned Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readUnsignedLong(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.ULONG0:
+                return 0l;
+            case EncodingCodes.SMALLULONG:
+                return ProtonStreamUtils.readByte(stream) & 0xffl;
+            case EncodingCodes.ULONG:
+                return ProtonStreamUtils.readLong(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Unsigned Long type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Float readFloat(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.FLOAT:
+                return Float.intBitsToFloat(ProtonStreamUtils.readInt(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Float type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public float readFloat(InputStream stream, StreamDecoderState state, float defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.FLOAT:
+                return Float.intBitsToFloat(ProtonStreamUtils.readInt(stream));
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Float type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Double readDouble(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.DOUBLE:
+                return Double.longBitsToDouble(ProtonStreamUtils.readLong(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Double type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public double readDouble(InputStream stream, StreamDecoderState state, double defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.DOUBLE:
+                return Double.longBitsToDouble(ProtonStreamUtils.readLong(stream));
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Double type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Binary readBinary(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                return binary8Decoder.readValue(stream, state);
+            case EncodingCodes.VBIN32:
+                return binary32Decoder.readValue(stream, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public ProtonBuffer readBinaryAsBuffer(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                return binary8Decoder.readValueAsBuffer(stream, state);
+            case EncodingCodes.VBIN32:
+                return binary32Decoder.readValueAsBuffer(stream, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public DeliveryTag readDeliveryTag(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                return new DeliveryTag.ProtonDeliveryTag(binary8Decoder.readValueAsArray(stream, state));
+            case EncodingCodes.VBIN32:
+                return new DeliveryTag.ProtonDeliveryTag(binary32Decoder.readValueAsArray(stream, state));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public String readString(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.STR8:
+                return string8Decoder.readValue(stream, state);
+            case EncodingCodes.STR32:
+                return string32Decoder.readValue(stream, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected String type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Symbol readSymbol(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SYM8:
+                return symbol8Decoder.readValue(stream, state);
+            case EncodingCodes.SYM32:
+                return symbol32Decoder.readValue(stream, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Symbol type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public String readSymbol(InputStream stream, StreamDecoderState state, String defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.SYM8:
+                return symbol8Decoder.readString(stream, state);
+            case EncodingCodes.SYM32:
+                return symbol32Decoder.readString(stream, state);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Symbol type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public Long readTimestamp(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.TIMESTAMP:
+                return ProtonStreamUtils.readLong(stream);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Timestamp type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public long readTimestamp(InputStream stream, StreamDecoderState state, long defaultValue) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.TIMESTAMP:
+                return ProtonStreamUtils.readLong(stream);
+            case EncodingCodes.NULL:
+                return defaultValue;
+            default:
+                throw new DecodeException("Expected Timestamp type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @Override
+    public UUID readUUID(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.UUID:
+                return new UUID(ProtonStreamUtils.readLong(stream), ProtonStreamUtils.readLong(stream));
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected UUID type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <K, V> Map<K, V> readMap(InputStream stream, StreamDecoderState state) throws DecodeException {
+         final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.MAP8:
+                return (Map<K, V>) map8Decoder.readValue(stream, state);
+            case EncodingCodes.MAP32:
+                return (Map<K, V>) map32Decoder.readValue(stream, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected Map type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <V> List<V> readList(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readEncodingCode(stream);
+
+        switch (encodingCode) {
+            case EncodingCodes.LIST0:
+                return Collections.emptyList();
+            case EncodingCodes.LIST8:
+                return (List<V>) list8Decoder.readValue(stream, state);
+            case EncodingCodes.LIST32:
+                return (List<V>) list32Decoder.readValue(stream, state);
+            case EncodingCodes.NULL:
+                return null;
+            default:
+                throw new DecodeException("Expected List type but found encoding: " + EncodingCodes.toString(encodingCode));
+        }
+    }
+
+    private ClassCastException signalUnexpectedType(final Object val, Class<?> clazz) {
+        return new ClassCastException("Unexpected type " + val.getClass().getName() +
+                                      ". Expected " + clazz.getName() + ".");
+    }
+
+    private StreamTypeDecoder<?> handleUnknownDescribedType(final Object descriptor) {
+        StreamTypeDecoder<?> StreamTypeDecoder = new UnknownDescribedTypeDecoder() {
+
+            @Override
+            public Object getDescriptor() {
+                return descriptor;
+            }
+        };
+
+        describedTypeDecoders.put(descriptor, (UnknownDescribedTypeDecoder) StreamTypeDecoder);
+
+        return StreamTypeDecoder;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderFactory.java
new file mode 100644
index 0000000..bc415f0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderFactory.java
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import org.apache.qpid.protonj2.codec.decoders.messaging.AcceptedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AmqpSequenceTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AmqpValueTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ApplicationPropertiesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DataTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnCloseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoLinksOrMessagesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoLinksTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoMessagesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeliveryAnnotationsTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.FooterTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.HeaderTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.MessageAnnotationsTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ModifiedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.PropertiesTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ReceivedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.RejectedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ReleasedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.SourceTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.TargetTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslChallengeTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslInitTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslMechanismsTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslOutcomeTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslResponseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.CoordinatorTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DeclareTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DeclaredTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DischargeTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.TransactionStateTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.AttachTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.BeginTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.CloseTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.DetachTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.DispositionTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.EndTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.ErrorConditionTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.FlowTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.OpenTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.TransferTypeDecoder;
+
+/**
+ * Factory that create and initializes new BuiltinDecoder instances
+ */
+public final class ProtonStreamDecoderFactory {
+
+    private ProtonStreamDecoderFactory() {
+    }
+
+    public static ProtonStreamDecoder create() {
+        ProtonStreamDecoder decoder = new ProtonStreamDecoder();
+
+        addMessagingTypeDecoders(decoder);
+        addTransactionTypeDecoders(decoder);
+        addTransportTypeDecoders(decoder);
+
+        return decoder;
+    }
+
+    public static ProtonStreamDecoder createSasl() {
+        ProtonStreamDecoder decoder = new ProtonStreamDecoder();
+
+        addSaslTypeDecoders(decoder);
+
+        return decoder;
+    }
+
+    private static void addMessagingTypeDecoders(ProtonStreamDecoder Decoder) {
+        Decoder.registerDescribedTypeDecoder(new AcceptedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new AmqpSequenceTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new AmqpValueTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ApplicationPropertiesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DataTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnCloseTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnNoLinksOrMessagesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnNoLinksTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeleteOnNoMessagesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeliveryAnnotationsTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new FooterTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new HeaderTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new MessageAnnotationsTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ModifiedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new PropertiesTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ReceivedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new RejectedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ReleasedTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new SourceTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new TargetTypeDecoder());
+    }
+
+    private static void addTransactionTypeDecoders(ProtonStreamDecoder Decoder) {
+        Decoder.registerDescribedTypeDecoder(new CoordinatorTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeclaredTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DeclareTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DischargeTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new TransactionStateTypeDecoder());
+    }
+
+    private static void addTransportTypeDecoders(ProtonStreamDecoder Decoder) {
+        Decoder.registerDescribedTypeDecoder(new AttachTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new BeginTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new CloseTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DetachTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new DispositionTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new EndTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new ErrorConditionTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new FlowTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new OpenTypeDecoder());
+        Decoder.registerDescribedTypeDecoder(new TransferTypeDecoder());
+    }
+
+    private static void addSaslTypeDecoders(ProtonStreamDecoder decoder) {
+        decoder.registerDescribedTypeDecoder(new SaslChallengeTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslInitTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslMechanismsTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslOutcomeTypeDecoder());
+        decoder.registerDescribedTypeDecoder(new SaslResponseTypeDecoder());
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderState.java
new file mode 100644
index 0000000..3f268c4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderState.java
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+
+/**
+ * State object used by the Built in Decoder implementation.
+ */
+public final class ProtonStreamDecoderState implements StreamDecoderState {
+
+    private static final int MAX_CHAR_BUFFER_CAHCE_SIZE = 100;
+
+    private final CharsetDecoder STRING_DECODER = StandardCharsets.UTF_8.newDecoder();
+    private final ProtonStreamDecoder decoder;
+    private final char[] decodeCache = new char[MAX_CHAR_BUFFER_CAHCE_SIZE];
+
+    private UTF8StreamDecoder stringDecoder;
+
+    public ProtonStreamDecoderState(ProtonStreamDecoder decoder) {
+        this.decoder = decoder;
+    }
+
+    @Override
+    public ProtonStreamDecoder getDecoder() {
+        return decoder;
+    }
+
+    @Override
+    public ProtonStreamDecoderState reset() {
+        // No intermediate state to reset
+        return this;
+    }
+
+    public UTF8StreamDecoder getStringDecoder() {
+        return stringDecoder;
+    }
+
+    public void setStringDecoder(UTF8StreamDecoder stringDecoder) {
+        this.stringDecoder = stringDecoder;
+    }
+
+    @Override
+    public String decodeUTF8(InputStream stream, int length) throws DecodeException {
+        try {
+            if (stringDecoder == null) {
+                return internalDecode(stream, length, STRING_DECODER, length > MAX_CHAR_BUFFER_CAHCE_SIZE ? new char[length] : decodeCache);
+            } else {
+                return stringDecoder.decodeUTF8(stream);
+            }
+        } catch (Exception ex) {
+            throw new DecodeException("Cannot parse encoded UTF8 String", ex);
+        }
+    }
+
+    private static String internalDecode(InputStream stream, final int length, CharsetDecoder decoder, char[] scratch) throws IOException {
+        int offset;
+        int lastRead = 0;
+
+        for (offset = 0; offset < length; offset++) {
+            lastRead = stream.read();
+            if (lastRead < 0) {
+                throw new EOFException("Reached end of stream before decoding the full String content");
+            } else if (lastRead > 127) {
+                break;
+            }
+            scratch[offset] = (char) lastRead;
+        }
+
+        if (offset == length) {
+            return new String(scratch, 0, length);
+        } else {
+            return internalDecodeUTF8(stream, length, scratch, (byte) lastRead, offset, decoder);
+        }
+    }
+
+    private static String internalDecodeUTF8(final InputStream stream, final int length, final char[] chars, final byte stoppageByte, final int offset, final CharsetDecoder decoder) throws IOException {
+        final CharBuffer out = CharBuffer.wrap(chars);
+        out.position(offset);
+
+        // Create a buffer from the remaining portion of the buffer and then use the decoder to complete the work
+        // remember to move the main buffer position to consume the data processed.
+        final byte[] trailingBytes = new byte[length - offset];
+        trailingBytes[0] = stoppageByte;
+        stream.read(trailingBytes, 1, trailingBytes.length - 1);
+        ByteBuffer byteBuffer = ByteBuffer.wrap(trailingBytes);
+
+        try {
+            for (;;) {
+                CoderResult cr = byteBuffer.hasRemaining() ? decoder.decode(byteBuffer, out, true) : CoderResult.UNDERFLOW;
+                if (cr.isUnderflow()) {
+                    cr = decoder.flush(out);
+                }
+                if (cr.isUnderflow()) {
+                    break;
+                }
+
+                // The char buffer should have been sufficient here but wasn't so we know
+                // that there was some encoding issue on the other end.
+                cr.throwException();
+            }
+
+            return out.flip().toString();
+        } catch (CharacterCodingException e) {
+            throw new DecodeException("Cannot parse encoded UTF8 String", e);
+        } finally {
+            decoder.reset();
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamUtils.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamUtils.java
new file mode 100644
index 0000000..d7d5254
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamUtils.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.qpid.protonj2.codec.DecodeEOFException;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+
+/**
+ * Set of Utility methods useful when dealing with byte arrays and other
+ * primitive types.
+ */
+public abstract class ProtonStreamUtils {
+
+    private static final byte[] EMPTY_ARRAY = new byte[0];
+
+    public static OutputStream writeByte(byte value, OutputStream stream) throws EncodeException {
+        try {
+            stream.write(value);
+        } catch (IOException ex) {
+            throw new DecodeException("Caught IO error writing to provided stream", ex);
+        }
+
+        return stream;
+    }
+
+    public static OutputStream writeShort(short value, OutputStream stream) throws EncodeException {
+        writeByte((byte) (value >>> 8), stream);
+        writeByte((byte) (value >>> 0), stream);
+
+        return stream;
+    }
+
+    public static OutputStream writeInt(int value, OutputStream stream) throws EncodeException {
+        writeByte((byte) (value >>> 24), stream);
+        writeByte((byte) (value >>> 16), stream);
+        writeByte((byte) (value >>> 8), stream);
+        writeByte((byte) (value >>> 0), stream);
+
+        return stream;
+    }
+
+    public static OutputStream writeLong(long value, OutputStream stream) throws EncodeException {
+        writeByte((byte) (value >>> 56), stream);
+        writeByte((byte) (value >>> 48), stream);
+        writeByte((byte) (value >>> 40), stream);
+        writeByte((byte) (value >>> 32), stream);
+        writeByte((byte) (value >>> 24), stream);
+        writeByte((byte) (value >>> 16), stream);
+        writeByte((byte) (value >>> 8), stream);
+        writeByte((byte) (value >>> 0), stream);
+
+        return stream;
+    }
+
+    public static byte[] readBytes(InputStream stream, int length) throws DecodeException {
+        try {
+            if (length == 0) {
+                return EMPTY_ARRAY;
+            } else {
+                final byte[] payload = new byte[length];
+
+                // NOTE: In JDK 11 we could use stream.readNBytes(length) which would allow
+                //       the stream to more efficiently provide the resulting bytes possibly
+                //       without a memory copy.
+
+                if (stream.read(payload) < length) {
+                    throw new DecodeException(String.format(
+                        "Failed to read requested number of bytes %d: instead only %d bytes were read.", length, payload.length));
+                }
+
+                return payload;
+            }
+        } catch (IOException ex) {
+            throw new DecodeException("Caught IO error reading from provided stream", ex);
+        }
+    }
+
+    public static byte readEncodingCode(InputStream stream) throws DecodeException {
+        try {
+            int result = stream.read();
+            if (result >= 0) {
+                return (byte) result;
+            } else {
+                throw new DecodeEOFException("Cannot read more type information from stream that has reached its end.");
+            }
+        } catch (IOException ex) {
+            throw new DecodeException("Caught IO error reading from provided stream", ex);
+        }
+    }
+
+    public static byte readByte(InputStream stream) throws DecodeException {
+        try {
+            int result = stream.read();
+            if (result >= 0) {
+                return (byte) result;
+            } else {
+                throw new DecodeException("Unexpectedly reached the end of the provided stream");
+            }
+        } catch (IOException ex) {
+            throw new DecodeException("Caught IO error reading from provided stream", ex);
+        }
+    }
+
+    public static short readShort(InputStream stream) {
+        return (short) ((readByte(stream) & 0xFF) << 8 |
+                        (readByte(stream) & 0xFF) << 0);
+    }
+
+    public static int readInt(InputStream stream) {
+        return (readByte(stream) & 0xFF) << 24 |
+               (readByte(stream) & 0xFF) << 16 |
+               (readByte(stream) & 0xFF) << 8 |
+               (readByte(stream) & 0xFF) << 0;
+    }
+
+    public static long readLong(InputStream stream) {
+        return (long) (readByte(stream) & 0xFF) << 56 |
+               (long) (readByte(stream) & 0xFF) << 48 |
+               (long) (readByte(stream) & 0xFF) << 40 |
+               (long) (readByte(stream) & 0xFF) << 32 |
+               (long) (readByte(stream) & 0xFF) << 24 |
+               (long) (readByte(stream) & 0xFF) << 16 |
+               (long) (readByte(stream) & 0xFF) << 8 |
+               (long) (readByte(stream) & 0xFF) << 0;
+    }
+
+    public static float readFloat(InputStream stream) {
+        return Float.intBitsToFloat(readInt(stream));
+    }
+
+    public static double readDouble(InputStream stream) {
+        return Double.longBitsToDouble(readLong(stream));
+    }
+
+    public static InputStream skipBytes(InputStream stream, long amount) {
+        try {
+            stream.skip(amount);
+        } catch (IOException ex) {
+            throw new DecodeException(
+                String.format("Error while attempting to skip %d bytes in the given InputStream", amount), ex);
+        }
+
+        return stream;
+    }
+
+    public static void reset(InputStream stream) throws DecodeException {
+        try {
+            stream.reset();
+        } catch (IOException ex) {
+            throw new DecodeException("Caught IO error when calling reset on provided stream", ex);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UTF8Decoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UTF8Decoder.java
new file mode 100644
index 0000000..505b872
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UTF8Decoder.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Interface for an external UTF8 Decoder that can be supplied by a client
+ * which implements custom decoding logic optimized for the application using
+ * the Codec.
+ */
+public interface UTF8Decoder {
+
+    /**
+     * Decodes a String from the given UTF8 Bytes advancing the buffer read index
+     * by the given length value once complete.  If the implementation does not advance
+     * the buffer read index the outcome of future decode calls is not defined.
+     *
+     * @param buffer
+     *      A ProtonBuffer containing the UTF-8 encoded bytes.
+     * @param utf8length
+     *      The number of bytes in the passed buffer that comprise the UTF-8 encoded value.
+     *
+     * @return a new String that represents the decoded value.
+     */
+    String decodeUTF8(ProtonBuffer buffer, int utf8length);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UTF8StreamDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UTF8StreamDecoder.java
new file mode 100644
index 0000000..6ba2faa
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UTF8StreamDecoder.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.io.InputStream;
+
+/**
+ * Interface for an external UTF8 Decoder that can be supplied by a client
+ * which implements custom decoding logic optimized for the application using
+ * the Codec.
+ */
+public interface UTF8StreamDecoder {
+
+    /**
+     * Decodes a String from the given UTF8 Bytes.
+     *
+     * @param utf8bytes
+     *      A {@link InputStream} containing the UTF-8 encoded bytes.
+     *
+     * @return a new String that represents the decoded value.
+     */
+    String decodeUTF8(InputStream utf8bytes);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UnknownDescribedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UnknownDescribedTypeDecoder.java
new file mode 100644
index 0000000..758845b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/UnknownDescribedTypeDecoder.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.types.DescribedType;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnknownDescribedType;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Decoder of AMQP Described type values from a byte stream.
+ */
+public abstract class UnknownDescribedTypeDecoder extends AbstractDescribedTypeDecoder<DescribedType> {
+
+    public abstract Object getDescriptor();
+
+    @Override
+    public final UnsignedLong getDescriptorCode() {
+        return getDescriptor() instanceof UnsignedLong ? (UnsignedLong) getDescriptor() : null;
+    }
+
+    @Override
+    public final Symbol getDescriptorSymbol() {
+        return getDescriptor() instanceof Symbol ? (Symbol) getDescriptor() : null;
+    }
+
+    @Override
+    public final Class<DescribedType> getTypeClass() {
+        return DescribedType.class;
+    }
+
+    @Override
+    public final DescribedType readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        Object described = decoder.readValue(buffer, state);
+
+        return new UnknownDescribedType(getDescriptor(), described);
+    }
+
+    @Override
+    public final DescribedType readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        Object described = decoder.readValue(stream, state);
+
+        return new UnknownDescribedType(getDescriptor(), described);
+    }
+
+    @Override
+    public final DescribedType[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        UnknownDescribedType[] result = new UnknownDescribedType[count];
+
+        for (int i = 0; i < count; ++i) {
+            Object described = decoder.readValue(buffer, state);
+            result[i] = new UnknownDescribedType(getDescriptor(), described);
+        }
+
+        return result;
+    }
+
+    @Override
+    public final DescribedType[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        UnknownDescribedType[] result = new UnknownDescribedType[count];
+
+        for (int i = 0; i < count; ++i) {
+            Object described = decoder.readValue(stream, state);
+            result[i] = new UnknownDescribedType(getDescriptor(), described);
+        }
+
+        return result;
+    }
+
+    @Override
+    public final void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        state.getDecoder().readNextTypeDecoder(buffer, state).skipValue(buffer, state);
+    }
+
+    @Override
+    public final void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        state.getDecoder().readNextTypeDecoder(stream, state).skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AcceptedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AcceptedTypeDecoder.java
new file mode 100644
index 0000000..0fca5dd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AcceptedTypeDecoder.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+
+/**
+ * Decoder of AMQP Accepted type values from a byte stream.
+ */
+public final class AcceptedTypeDecoder extends AbstractDescribedTypeDecoder<Accepted> {
+
+    @Override
+    public Class<Accepted> getTypeClass() {
+        return Accepted.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Accepted.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Accepted.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Accepted readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+
+        return Accepted.getInstance();
+    }
+
+    @Override
+    public Accepted[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final Accepted[] result = new Accepted[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(buffer, state);
+            result[i] = Accepted.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public Accepted readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+
+        return Accepted.getInstance();
+    }
+
+    @Override
+    public Accepted[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final Accepted[] result = new Accepted[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(stream, state);
+            result[i] = Accepted.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AmqpSequenceTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AmqpSequenceTypeDecoder.java
new file mode 100644
index 0000000..377e042
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AmqpSequenceTypeDecoder.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+
+/**
+ * Decoder of AMQP Data type values from a byte stream.
+ */
+@SuppressWarnings("rawtypes")
+public final class AmqpSequenceTypeDecoder extends AbstractDescribedTypeDecoder<AmqpSequence> {
+
+    @Override
+    public Class<AmqpSequence> getTypeClass() {
+        return AmqpSequence.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return AmqpSequence.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return AmqpSequence.DESCRIPTOR_SYMBOL;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public AmqpSequence<?> readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        final ListTypeDecoder valueDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+        final List<Object> result = valueDecoder.readValue(buffer, state);
+
+        return new AmqpSequence<>(result);
+    }
+
+    @SuppressWarnings({ "unchecked" })
+    @Override
+    public AmqpSequence[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        final ListTypeDecoder valueDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+        final List<Object>[] elements = valueDecoder.readArrayElements(buffer, state, count);
+
+        AmqpSequence[] array = new AmqpSequence[count];
+        for (int i = 0; i < count; ++i) {
+            array[i] = new AmqpSequence(elements[i]);
+        }
+
+        return array;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public AmqpSequence readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        final ListTypeDecoder valueDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+        final List<Object> result = valueDecoder.readValue(stream, state);
+
+        return new AmqpSequence<>(result);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public AmqpSequence[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        final ListTypeDecoder valueDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+        final List<Object>[] elements = valueDecoder.readArrayElements(stream, state, count);
+
+        AmqpSequence[] array = new AmqpSequence[count];
+        for (int i = 0; i < count; ++i) {
+            array[i] = new AmqpSequence(elements[i]);
+        }
+
+        return array;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AmqpValueTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AmqpValueTypeDecoder.java
new file mode 100644
index 0000000..c0ebea9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/AmqpValueTypeDecoder.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+
+/**
+ * Decoder of AMQP Data type values from a byte stream.
+ */
+@SuppressWarnings("rawtypes")
+public final class AmqpValueTypeDecoder extends AbstractDescribedTypeDecoder<AmqpValue> {
+
+    @Override
+    public Class<AmqpValue> getTypeClass() {
+        return AmqpValue.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return AmqpValue.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return AmqpValue.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public AmqpValue<?> readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        return new AmqpValue<>(decoder.readValue(buffer, state));
+    }
+
+    @Override
+    public AmqpValue[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Object[] elements = decoder.readArrayElements(buffer, state, count);
+
+        final AmqpValue[] array = new AmqpValue[count];
+        for (int i = 0; i < count; ++i) {
+            array[i] = new AmqpValue<>(elements[i]);
+        }
+
+        return array;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public AmqpValue readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        return new AmqpValue<>(decoder.readValue(stream, state));
+    }
+
+    @Override
+    public AmqpValue[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Object[] elements = decoder.readArrayElements(stream, state, count);
+
+        final AmqpValue[] array = new AmqpValue[count];
+        for (int i = 0; i < count; ++i) {
+            array[i] = new AmqpValue<>(elements[i]);
+        }
+
+        return array;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ApplicationPropertiesTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ApplicationPropertiesTypeDecoder.java
new file mode 100644
index 0000000..1a745fd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ApplicationPropertiesTypeDecoder.java
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoder;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.MapTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+
+/**
+ * Decoder of AMQP ApplicationProperties types from a byte stream
+ */
+public final class ApplicationPropertiesTypeDecoder extends AbstractDescribedTypeDecoder<ApplicationProperties> {
+
+    @Override
+    public Class<ApplicationProperties> getTypeClass() {
+        return ApplicationProperties.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return ApplicationProperties.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return ApplicationProperties.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public ApplicationProperties readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            return new ApplicationProperties(null);
+        }
+
+        return new ApplicationProperties(readMap(buffer, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+    }
+
+    @Override
+    public ApplicationProperties[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final ApplicationProperties[] result = new ApplicationProperties[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                result[i] = new ApplicationProperties(null);
+            }
+            return result;
+        }
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new ApplicationProperties(readMap(buffer, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(buffer, state);
+        }
+    }
+
+    private Map<String, Object> readMap(ProtonBuffer buffer, DecoderState state, MapTypeDecoder mapDecoder) throws DecodeException {
+        final int size = mapDecoder.readSize(buffer);
+        final int count = mapDecoder.readCount(buffer);
+
+        if (count > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "Map encoded size %d is specified to be greater than the amount " +
+                    "of data available (%d)", size, buffer.getReadableBytes()));
+        }
+
+        final Decoder decoder = state.getDecoder();
+
+        // Count include both key and value so we must include that in the loop
+        final Map<String, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            String key = decoder.readString(buffer, state);
+            Object value = decoder.readObject(buffer, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+
+    @Override
+    public ApplicationProperties readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            return new ApplicationProperties(null);
+        }
+
+        return new ApplicationProperties(readMap(stream, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+    }
+
+    @Override
+    public ApplicationProperties[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final ApplicationProperties[] result = new ApplicationProperties[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                result[i] = new ApplicationProperties(null);
+            }
+            return result;
+        }
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new ApplicationProperties(readMap(stream, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(stream, state);
+        }
+    }
+
+    private Map<String, Object> readMap(InputStream stream, StreamDecoderState state, MapTypeDecoder mapDecoder) throws DecodeException {
+        @SuppressWarnings("unused")
+        final int size = mapDecoder.readSize(stream);
+        final int count = mapDecoder.readCount(stream);
+
+        final StreamDecoder decoder = state.getDecoder();
+
+        // Count include both key and value so we must include that in the loop
+        final Map<String, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            String key = decoder.readString(stream, state);
+            Object value = decoder.readObject(stream, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DataTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DataTypeDecoder.java
new file mode 100644
index 0000000..5b27467
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DataTypeDecoder.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BinaryTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Data;
+
+/**
+ * Decoder of AMQP Data type values from a byte stream.
+ */
+public final class DataTypeDecoder extends AbstractDescribedTypeDecoder<Data> {
+
+    private static final Data EMPTY_DATA = new Data((Binary) null);
+
+    @Override
+    public Class<Data> getTypeClass() {
+        return Data.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Data.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Data.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Data readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final byte encodingCode = buffer.readByte();
+        final int size;
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                size = buffer.readByte() & 0xFF;
+                break;
+            case EncodingCodes.VBIN32:
+                size = buffer.readInt();
+                break;
+            case EncodingCodes.NULL:
+                return EMPTY_DATA;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + encodingCode);
+        }
+
+        if (size > buffer.getReadableBytes()) {
+            throw new DecodeException("Binary data size " + size + " is specified to be greater than the " +
+                                      "amount of data available ("+ buffer.getReadableBytes()+")");
+        }
+
+        final int position = buffer.getReadIndex();
+        final ProtonBuffer data = ProtonByteBufferAllocator.DEFAULT.allocate(size, size);
+
+        buffer.getBytes(position, data.getArray(), data.getArrayOffset(), size);
+        data.setWriteIndex(size);
+        buffer.setReadIndex(position + size);
+
+        return new Data(new Binary(data));
+    }
+
+    @Override
+    public Data[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        final BinaryTypeDecoder valueDecoder = checkIsExpectedTypeAndCast(BinaryTypeDecoder.class, decoder);
+        final Binary[] binaryArray = valueDecoder.readArrayElements(buffer, state, count);
+
+        final Data[] dataArray = new Data[count];
+        for (int i = 0; i < count; ++i) {
+            dataArray[i] = new Data(binaryArray[i]);
+        }
+
+        return dataArray;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(BinaryTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public Data readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final byte encodingCode = ProtonStreamUtils.readByte(stream);
+        final int size;
+
+        switch (encodingCode) {
+            case EncodingCodes.VBIN8:
+                size = ProtonStreamUtils.readByte(stream) & 0xFF;
+                break;
+            case EncodingCodes.VBIN32:
+                size = ProtonStreamUtils.readInt(stream);
+                break;
+            case EncodingCodes.NULL:
+                return EMPTY_DATA;
+            default:
+                throw new DecodeException("Expected Binary type but found encoding: " + encodingCode);
+        }
+
+        return new Data(new Binary(ProtonStreamUtils.readBytes(stream, size)));
+    }
+
+    @Override
+    public Data[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        final BinaryTypeDecoder valueDecoder = checkIsExpectedTypeAndCast(BinaryTypeDecoder.class, decoder);
+        final Binary[] binaryArray = valueDecoder.readArrayElements(stream, state, count);
+
+        final Data[] dataArray = new Data[count];
+        for (int i = 0; i < count; ++i) {
+            dataArray[i] = new Data(binaryArray[i]);
+        }
+
+        return dataArray;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(BinaryTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnCloseTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnCloseTypeDecoder.java
new file mode 100644
index 0000000..5eaeae2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnCloseTypeDecoder.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnClose;
+
+/**
+ * Decoder of AMQP DeleteOnClose type values from a byte stream
+ */
+public final class DeleteOnCloseTypeDecoder extends AbstractDescribedTypeDecoder<DeleteOnClose> {
+
+    @Override
+    public Class<DeleteOnClose> getTypeClass() {
+        return DeleteOnClose.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnClose.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnClose.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public DeleteOnClose readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+
+        return DeleteOnClose.getInstance();
+    }
+
+    @Override
+    public DeleteOnClose[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnClose[] result = new DeleteOnClose[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(buffer, state);
+            result[i] = DeleteOnClose.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public DeleteOnClose readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+
+        return DeleteOnClose.getInstance();
+    }
+
+    @Override
+    public DeleteOnClose[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnClose[] result = new DeleteOnClose[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(stream, state);
+            result[i] = DeleteOnClose.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoLinksOrMessagesTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoLinksOrMessagesTypeDecoder.java
new file mode 100644
index 0000000..0991aa0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoLinksOrMessagesTypeDecoder.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinksOrMessages;
+
+/**
+ * Decoder of AMQP DeleteOnNoLinksOrMessages type values from a byte stream
+ */
+public final class DeleteOnNoLinksOrMessagesTypeDecoder extends AbstractDescribedTypeDecoder<DeleteOnNoLinksOrMessages> {
+
+    @Override
+    public Class<DeleteOnNoLinksOrMessages> getTypeClass() {
+        return DeleteOnNoLinksOrMessages.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnNoLinksOrMessages.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public DeleteOnNoLinksOrMessages readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+
+        return DeleteOnNoLinksOrMessages.getInstance();
+    }
+
+    @Override
+    public DeleteOnNoLinksOrMessages[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnNoLinksOrMessages[] result = new DeleteOnNoLinksOrMessages[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(buffer, state);
+            result[i] = DeleteOnNoLinksOrMessages.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public DeleteOnNoLinksOrMessages readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+
+        return DeleteOnNoLinksOrMessages.getInstance();
+    }
+
+    @Override
+    public DeleteOnNoLinksOrMessages[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnNoLinksOrMessages[] result = new DeleteOnNoLinksOrMessages[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(stream, state);
+            result[i] = DeleteOnNoLinksOrMessages.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoLinksTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoLinksTypeDecoder.java
new file mode 100644
index 0000000..c507622
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoLinksTypeDecoder.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinks;
+
+/**
+ * Decoder of AMQP DeleteOnNoLinks type values from a byte stream
+ */
+public final class DeleteOnNoLinksTypeDecoder extends AbstractDescribedTypeDecoder<DeleteOnNoLinks> {
+
+    @Override
+    public Class<DeleteOnNoLinks> getTypeClass() {
+        return DeleteOnNoLinks.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnNoLinks.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnNoLinks.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public DeleteOnNoLinks readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+
+        return DeleteOnNoLinks.getInstance();
+    }
+
+    @Override
+    public DeleteOnNoLinks[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnNoLinks[] result = new DeleteOnNoLinks[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(buffer, state);
+            result[i] = DeleteOnNoLinks.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public DeleteOnNoLinks readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+
+        return DeleteOnNoLinks.getInstance();
+    }
+
+    @Override
+    public DeleteOnNoLinks[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnNoLinks[] result = new DeleteOnNoLinks[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(stream, state);
+            result[i] = DeleteOnNoLinks.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoMessagesTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoMessagesTypeDecoder.java
new file mode 100644
index 0000000..2f84b5a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeleteOnNoMessagesTypeDecoder.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoMessages;
+
+/**
+ * Decoder of AMQP DeleteOnNoLinks type values from a byte stream
+ */
+public final class DeleteOnNoMessagesTypeDecoder extends AbstractDescribedTypeDecoder<DeleteOnNoMessages> {
+
+    @Override
+    public Class<DeleteOnNoMessages> getTypeClass() {
+        return DeleteOnNoMessages.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnNoMessages.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnNoMessages.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public DeleteOnNoMessages readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+
+        return DeleteOnNoMessages.getInstance();
+    }
+
+    @Override
+    public DeleteOnNoMessages[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnNoMessages[] result = new DeleteOnNoMessages[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(buffer, state);
+            result[i] = DeleteOnNoMessages.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public DeleteOnNoMessages readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+
+        return DeleteOnNoMessages.getInstance();
+    }
+
+    @Override
+    public DeleteOnNoMessages[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final DeleteOnNoMessages[] result = new DeleteOnNoMessages[count];
+
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(stream, state);
+            result[i] = DeleteOnNoMessages.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeliveryAnnotationsTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeliveryAnnotationsTypeDecoder.java
new file mode 100644
index 0000000..1c8e42d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/DeliveryAnnotationsTypeDecoder.java
@@ -0,0 +1,180 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.MapTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+
+/**
+ * Decoder of AMQP Delivery Annotations type values from a byte stream.
+ */
+public final class DeliveryAnnotationsTypeDecoder extends AbstractDescribedTypeDecoder<DeliveryAnnotations> {
+
+    @Override
+    public Class<DeliveryAnnotations> getTypeClass() {
+        return DeliveryAnnotations.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeliveryAnnotations.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeliveryAnnotations.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public DeliveryAnnotations readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            decoder.readValue(buffer, state);
+            return new DeliveryAnnotations(null);
+        }
+
+        return new DeliveryAnnotations(readMap(buffer, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+    }
+
+    @Override
+    public DeliveryAnnotations[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final DeliveryAnnotations[] result = new DeliveryAnnotations[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                decoder.readValue(buffer, state);
+                result[i] = new DeliveryAnnotations(null);
+            }
+            return result;
+        }
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new DeliveryAnnotations(readMap(buffer, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(buffer, state);
+        }
+    }
+
+    private Map<Symbol, Object> readMap(ProtonBuffer buffer, DecoderState state, MapTypeDecoder mapDecoder) throws DecodeException {
+        final int size = mapDecoder.readSize(buffer);
+        final int count = mapDecoder.readCount(buffer);
+
+        if (count > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "Map encoded size %d is specified to be greater than the amount " +
+                    "of data available (%d)", size, buffer.getReadableBytes()));
+        }
+
+        // Count include both key and value so we must include that in the loop
+        final Map<Symbol, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            Symbol key = state.getDecoder().readSymbol(buffer, state);
+            Object value = state.getDecoder().readObject(buffer, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+
+    @Override
+    public DeliveryAnnotations readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            decoder.readValue(stream, state);
+            return new DeliveryAnnotations(null);
+        }
+
+        return new DeliveryAnnotations(readMap(stream, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+    }
+
+    @Override
+    public DeliveryAnnotations[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final DeliveryAnnotations[] result = new DeliveryAnnotations[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                decoder.readValue(stream, state);
+                result[i] = new DeliveryAnnotations(null);
+            }
+            return result;
+        }
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new DeliveryAnnotations(readMap(stream, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(stream, state);
+        }
+    }
+
+    private Map<Symbol, Object> readMap(InputStream stream, StreamDecoderState state, MapTypeDecoder mapDecoder) throws DecodeException {
+        @SuppressWarnings("unused")
+        final int size = mapDecoder.readSize(stream);
+        final int count = mapDecoder.readCount(stream);
+
+        // Count include both key and value so we must include that in the loop
+        final Map<Symbol, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            Symbol key = state.getDecoder().readSymbol(stream, state);
+            Object value = state.getDecoder().readObject(stream, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/FooterTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/FooterTypeDecoder.java
new file mode 100644
index 0000000..462eccd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/FooterTypeDecoder.java
@@ -0,0 +1,151 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.MapTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+
+/**
+ * Decoder of AMQP Footer type values from a byte stream.
+ */
+public final class FooterTypeDecoder extends AbstractDescribedTypeDecoder<Footer> {
+
+    @Override
+    public Class<Footer> getTypeClass() {
+        return Footer.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Footer.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Footer.DESCRIPTOR_SYMBOL;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Footer readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            decoder.readValue(buffer, state);
+            return new Footer(null);
+        }
+
+        MapTypeDecoder mapDecoder = checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder);
+
+        return new Footer(mapDecoder.readValue(buffer, state));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Footer[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Footer[] result = new Footer[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                decoder.readValue(buffer, state);
+                result[i] = new Footer(null);
+            }
+            return result;
+        }
+
+        final MapTypeDecoder mapDecoder = checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder);
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new Footer(mapDecoder.readValue(buffer, state));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(buffer, state);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Footer readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            decoder.readValue(stream, state);
+            return new Footer(null);
+        }
+
+        final MapTypeDecoder mapDecoder = checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder);
+
+        return new Footer(mapDecoder.readValue(stream, state));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Footer[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Footer[] result = new Footer[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                decoder.readValue(stream, state);
+                result[i] = new Footer(null);
+            }
+            return result;
+        }
+
+        final MapTypeDecoder mapDecoder = checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder);
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new Footer(mapDecoder.readValue(stream, state));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(stream, state);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/HeaderTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/HeaderTypeDecoder.java
new file mode 100644
index 0000000..14d0484
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/HeaderTypeDecoder.java
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Header;
+
+/**
+ * Decoder of AMQP Header types from a byte stream
+ */
+public final class HeaderTypeDecoder extends AbstractDescribedTypeDecoder<Header> {
+
+    private static final int MIN_HEADER_LIST_ENTRIES = 0;
+    private static final int MAX_HEADER_LIST_ENTRIES = 5;
+
+    @Override
+    public Class<Header> getTypeClass() {
+        return Header.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Header.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Header.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Header readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readHeader(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Header[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        final ListTypeDecoder listDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+
+        final Header[] result = new Header[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readHeader(buffer, state, listDecoder);
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Header readHeader(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Header header = new Header();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_HEADER_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Header list encoding: " + count);
+        }
+
+        if (count > MAX_HEADER_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Header list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    header.setDurable(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 1:
+                    header.setPriority(state.getDecoder().readUnsignedByte(buffer, state, Header.DEFAULT_PRIORITY));
+                    break;
+                case 2:
+                    header.setTimeToLive(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 3:
+                    header.setFirstAcquirer(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 4:
+                    header.setDeliveryCount(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+            }
+        }
+
+        return header;
+    }
+
+    @Override
+    public Header readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readHeader(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Header[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        final ListTypeDecoder listDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+
+        final Header[] result = new Header[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readHeader(stream, state, listDecoder);
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Header readHeader(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Header header = new Header();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_HEADER_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Header list encoding: " + count);
+        }
+
+        if (count > MAX_HEADER_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Header list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                if (ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL) {
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    header.setDurable(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 1:
+                    header.setPriority(state.getDecoder().readUnsignedByte(stream, state, Header.DEFAULT_PRIORITY));
+                    break;
+                case 2:
+                    header.setTimeToLive(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 3:
+                    header.setFirstAcquirer(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 4:
+                    header.setDeliveryCount(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+            }
+        }
+
+        return header;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/MessageAnnotationsTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/MessageAnnotationsTypeDecoder.java
new file mode 100644
index 0000000..3e25ef6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/MessageAnnotationsTypeDecoder.java
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.MapTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+
+/**
+ * Decoder of AMQP Message Annotations type values from a byte stream.
+ */
+public final class MessageAnnotationsTypeDecoder extends AbstractDescribedTypeDecoder<MessageAnnotations> {
+
+    @Override
+    public Class<MessageAnnotations> getTypeClass() {
+        return MessageAnnotations.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return MessageAnnotations.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return MessageAnnotations.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public MessageAnnotations readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            decoder.readValue(buffer, state);
+            return new MessageAnnotations(null);
+        }
+
+        return new MessageAnnotations(readMap(buffer, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+    }
+
+    @Override
+    public MessageAnnotations[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        final MessageAnnotations[] result = new MessageAnnotations[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                decoder.readValue(buffer, state);
+                result[i] = new MessageAnnotations(null);
+            }
+            return result;
+        }
+
+        final MapTypeDecoder mapDecoder = checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder);
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new MessageAnnotations(readMap(buffer, state, mapDecoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(buffer, state);
+        }
+    }
+
+    private Map<Symbol, Object> readMap(ProtonBuffer buffer, DecoderState state, MapTypeDecoder mapDecoder) throws DecodeException {
+        final int size = mapDecoder.readSize(buffer);
+        final int count = mapDecoder.readCount(buffer);
+
+        if (count > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "Map encoded size %d is specified to be greater than the amount " +
+                    "of data available (%d)", size, buffer.getReadableBytes()));
+        }
+
+        // Count include both key and value so we must include that in the loop
+        final Map<Symbol, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            Symbol key = state.getDecoder().readSymbol(buffer, state);
+            Object value = state.getDecoder().readObject(buffer, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+
+    @Override
+    public MessageAnnotations readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (decoder instanceof NullTypeDecoder) {
+            decoder.readValue(stream, state);
+            return new MessageAnnotations(null);
+        }
+
+        return new MessageAnnotations(readMap(stream, state, checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder)));
+    }
+
+    @Override
+    public MessageAnnotations[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        final MessageAnnotations[] result = new MessageAnnotations[count];
+
+        if (decoder instanceof NullTypeDecoder) {
+            for (int i = 0; i < count; ++i) {
+                decoder.readValue(stream, state);
+                result[i] = new MessageAnnotations(null);
+            }
+            return result;
+        }
+
+        final MapTypeDecoder mapDecoder = checkIsExpectedTypeAndCast(MapTypeDecoder.class, decoder);
+
+        for (int i = 0; i < count; ++i) {
+            result[i] = new MessageAnnotations(readMap(stream, state, mapDecoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (!(decoder instanceof NullTypeDecoder)) {
+            checkIsExpectedType(MapTypeDecoder.class, decoder);
+            decoder.skipValue(stream, state);
+        }
+    }
+
+    private Map<Symbol, Object> readMap(InputStream stream, StreamDecoderState state, MapTypeDecoder mapDecoder) throws DecodeException {
+        @SuppressWarnings("unused")
+        final int size = mapDecoder.readSize(stream);
+        final int count = mapDecoder.readCount(stream);
+
+        // Count include both key and value so we must include that in the loop
+        final Map<Symbol, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            Symbol key = state.getDecoder().readSymbol(stream, state);
+            Object value = state.getDecoder().readObject(stream, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ModifiedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ModifiedTypeDecoder.java
new file mode 100644
index 0000000..1f1113e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ModifiedTypeDecoder.java
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+
+/**
+ * Decoder of AMQP Modified type values from a byte stream.
+ */
+public final class ModifiedTypeDecoder extends AbstractDescribedTypeDecoder<Modified> {
+
+    private static final int MIN_MODIFIED_LIST_ENTRIES = 0;
+    private static final int MAX_MODIFIED_LIST_ENTRIES = 3;
+
+    @Override
+    public Class<Modified> getTypeClass() {
+        return Modified.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Modified.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Modified.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Modified readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readModified(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Modified[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Modified[] result = new Modified[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readModified(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Modified readModified(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Modified modified = new Modified();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_MODIFIED_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Modified list encoding: " + count);
+        }
+
+        if (count > MAX_MODIFIED_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Modified list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    modified.setDeliveryFailed(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 1:
+                    modified.setUndeliverableHere(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 2:
+                    modified.setMessageAnnotations(state.getDecoder().readMap(buffer, state));
+                    break;
+                default:
+                    throw new DecodeException("To many entries in Modified encoding");
+            }
+        }
+
+        return modified;
+    }
+
+    @Override
+    public Modified readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        return readModified(stream, state, (ListTypeDecoder) decoder);
+    }
+
+    @Override
+    public Modified[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Modified[] result = new Modified[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readModified(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Modified readModified(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Modified modified = new Modified();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_MODIFIED_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Modified list encoding: " + count);
+        }
+
+        if (count > MAX_MODIFIED_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Modified list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    modified.setDeliveryFailed(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 1:
+                    modified.setUndeliverableHere(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 2:
+                    modified.setMessageAnnotations(state.getDecoder().readMap(stream, state));
+                    break;
+                default:
+                    throw new DecodeException("To many entries in Modified encoding");
+            }
+        }
+
+        return modified;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/PropertiesTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/PropertiesTypeDecoder.java
new file mode 100644
index 0000000..8fc0aa6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/PropertiesTypeDecoder.java
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+
+/**
+ * Decoder of AMQP Properties type values from a byte stream
+ */
+public final class PropertiesTypeDecoder extends AbstractDescribedTypeDecoder<Properties> {
+
+    private static final int MIN_PROPERTIES_LIST_ENTRIES = 0;
+    private static final int MAX_PROPERTIES_LIST_ENTRIES = 13;
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Properties.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Properties.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Properties> getTypeClass() {
+        return Properties.class;
+    }
+
+    @Override
+    public Properties readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Properties[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+        final ListTypeDecoder listDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+
+        final Properties[] result = new Properties[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(buffer, state, listDecoder);
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Properties readProperties(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Properties properties = new Properties();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_PROPERTIES_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Properties list encoding: " + count);
+        }
+
+        if (count > MAX_PROPERTIES_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Properties list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    properties.setMessageId(state.getDecoder().readObject(buffer, state));
+                    break;
+                case 1:
+                    properties.setUserId(state.getDecoder().readBinary(buffer, state));
+                    break;
+                case 2:
+                    properties.setTo(state.getDecoder().readString(buffer, state));
+                    break;
+                case 3:
+                    properties.setSubject(state.getDecoder().readString(buffer, state));
+                    break;
+                case 4:
+                    properties.setReplyTo(state.getDecoder().readString(buffer, state));
+                    break;
+                case 5:
+                    properties.setCorrelationId(state.getDecoder().readObject(buffer, state));
+                    break;
+                case 6:
+                    properties.setContentType(state.getDecoder().readSymbol(buffer, state, null));
+                    break;
+                case 7:
+                    properties.setContentEncoding(state.getDecoder().readSymbol(buffer, state, null));
+                    break;
+                case 8:
+                    properties.setAbsoluteExpiryTime(state.getDecoder().readTimestamp(buffer, state, 0l));
+                    break;
+                case 9:
+                    properties.setCreationTime(state.getDecoder().readTimestamp(buffer, state, 0l));
+                    break;
+                case 10:
+                    properties.setGroupId(state.getDecoder().readString(buffer, state));
+                    break;
+                case 11:
+                    properties.setGroupSequence(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 12:
+                    properties.setReplyToGroupId(state.getDecoder().readString(buffer, state));
+                    break;
+            }
+        }
+
+        return properties;
+    }
+
+    @Override
+    public Properties readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Properties[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+        final ListTypeDecoder listDecoder = checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder);
+
+        final Properties[] result = new Properties[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(stream, state, listDecoder);
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Properties readProperties(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Properties properties = new Properties();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_PROPERTIES_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Properties list encoding: " + count);
+        }
+
+        if (count > MAX_PROPERTIES_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Properties list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                if (ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL) {
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    properties.setMessageId(state.getDecoder().readObject(stream, state));
+                    break;
+                case 1:
+                    properties.setUserId(state.getDecoder().readBinary(stream, state));
+                    break;
+                case 2:
+                    properties.setTo(state.getDecoder().readString(stream, state));
+                    break;
+                case 3:
+                    properties.setSubject(state.getDecoder().readString(stream, state));
+                    break;
+                case 4:
+                    properties.setReplyTo(state.getDecoder().readString(stream, state));
+                    break;
+                case 5:
+                    properties.setCorrelationId(state.getDecoder().readObject(stream, state));
+                    break;
+                case 6:
+                    properties.setContentType(state.getDecoder().readSymbol(stream, state, null));
+                    break;
+                case 7:
+                    properties.setContentEncoding(state.getDecoder().readSymbol(stream, state, null));
+                    break;
+                case 8:
+                    properties.setAbsoluteExpiryTime(state.getDecoder().readTimestamp(stream, state, 0l));
+                    break;
+                case 9:
+                    properties.setCreationTime(state.getDecoder().readTimestamp(stream, state, 0l));
+                    break;
+                case 10:
+                    properties.setGroupId(state.getDecoder().readString(stream, state));
+                    break;
+                case 11:
+                    properties.setGroupSequence(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 12:
+                    properties.setReplyToGroupId(state.getDecoder().readString(stream, state));
+                    break;
+            }
+        }
+
+        return properties;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ReceivedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ReceivedTypeDecoder.java
new file mode 100644
index 0000000..e7dbc22
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ReceivedTypeDecoder.java
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Received;
+
+/**
+ * Decoder of AMQP Received type value from a byte stream.
+ */
+public final class ReceivedTypeDecoder extends AbstractDescribedTypeDecoder<Received> {
+
+    private static final int REQUIRED_RECEIVED_LIST_ENTRIES = 2;
+
+    @Override
+    public Class<Received> getTypeClass() {
+        return Received.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Received.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Received.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Received readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readReceived(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Received[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Received[] result = new Received[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readReceived(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Received readReceived(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Received received = new Received();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count != REQUIRED_RECEIVED_LIST_ENTRIES) {
+            throw new DecodeException("Invalid number of entries in Received list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    received.setSectionNumber(state.getDecoder().readUnsignedInteger(buffer, state));
+                    break;
+                case 1:
+                    received.setSectionOffset(state.getDecoder().readUnsignedLong(buffer, state));
+                    break;
+            }
+        }
+
+        return received;
+    }
+
+    @Override
+    public Received readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readReceived(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Received[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Received[] result = new Received[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readReceived(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Received readReceived(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Received received = new Received();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count != REQUIRED_RECEIVED_LIST_ENTRIES) {
+            throw new DecodeException("Invalid number of entries in Received list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    received.setSectionNumber(state.getDecoder().readUnsignedInteger(stream, state));
+                    break;
+                case 1:
+                    received.setSectionOffset(state.getDecoder().readUnsignedLong(stream, state));
+                    break;
+            }
+        }
+
+        return received;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/RejectedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/RejectedTypeDecoder.java
new file mode 100644
index 0000000..5a2425c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/RejectedTypeDecoder.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Decoder of AMQP Rejected type values from a byte stream.
+ */
+public final class RejectedTypeDecoder extends AbstractDescribedTypeDecoder<Rejected> {
+
+    private static final int MIN_REJECTED_LIST_ENTRIES = 0;
+    private static final int MAX_REJECTED_LIST_ENTRIES = 1;
+
+    @Override
+    public Class<Rejected> getTypeClass() {
+        return Rejected.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Rejected.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Rejected.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Rejected readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        return readRejected(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Rejected[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Rejected[] result = new Rejected[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readRejected(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Rejected readRejected(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Rejected rejected = new Rejected();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_REJECTED_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Rejected list encoding: " + count);
+        }
+
+        if (count > MAX_REJECTED_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Rejected list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    rejected.setError(state.getDecoder().readObject(buffer, state, ErrorCondition.class));
+                    break;
+            }
+        }
+
+        return rejected;
+    }
+
+    @Override
+    public Rejected readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readRejected(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Rejected[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Rejected[] result = new Rejected[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readRejected(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Rejected readRejected(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Rejected rejected = new Rejected();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_REJECTED_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Rejected list encoding: " + count);
+        }
+
+        if (count > MAX_REJECTED_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Rejected list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    rejected.setError(state.getDecoder().readObject(stream, state, ErrorCondition.class));
+                    break;
+            }
+        }
+
+        return rejected;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ReleasedTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ReleasedTypeDecoder.java
new file mode 100644
index 0000000..ba7683f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/ReleasedTypeDecoder.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Released;
+
+/**
+ * Decoder of AMQP Released type values from a byte stream.
+ */
+public final class ReleasedTypeDecoder extends AbstractDescribedTypeDecoder<Released> {
+
+    @Override
+    public Class<Released> getTypeClass() {
+        return Released.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Released.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Released.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Released readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+
+        return Released.getInstance();
+    }
+
+    @Override
+    public Released[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final Released[] result = new Released[count];
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(buffer, state);
+            result[i] = Released.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public Released readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+
+        return Released.getInstance();
+    }
+
+    @Override
+    public Released[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        final Released[] result = new Released[count];
+        for (int i = 0; i < count; ++i) {
+            decoder.skipValue(stream, state);
+            result[i] = Released.getInstance();
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/SourceTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/SourceTypeDecoder.java
new file mode 100644
index 0000000..16264df
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/SourceTypeDecoder.java
@@ -0,0 +1,233 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+
+/**
+ * Decoder of AMQP Source type values from a byte stream.
+ */
+public final class SourceTypeDecoder extends AbstractDescribedTypeDecoder<Source> {
+
+    private static final int MIN_SOURCE_LIST_ENTRIES = 0;
+    private static final int MAX_SOURCE_LIST_ENTRIES = 11;
+
+    @Override
+    public Class<Source> getTypeClass() {
+        return Source.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Source.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Source.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Source readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readSource(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Source[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Source[] result = new Source[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readSource(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Source readSource(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Source source = new Source();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_SOURCE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Source list encoding: " + count);
+        }
+
+        if (count > MAX_SOURCE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Source list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    source.setAddress(state.getDecoder().readString(buffer, state));
+                    break;
+                case 1:
+                    final long durability = state.getDecoder().readUnsignedInteger(buffer, state, 0);
+                    source.setDurable(TerminusDurability.valueOf(durability));
+                    break;
+                case 2:
+                    final Symbol expiryPolicy = state.getDecoder().readSymbol(buffer, state);
+                    source.setExpiryPolicy(expiryPolicy == null ? TerminusExpiryPolicy.SESSION_END : TerminusExpiryPolicy.valueOf(expiryPolicy));
+                    break;
+                case 3:
+                    final UnsignedInteger timeout = state.getDecoder().readUnsignedInteger(buffer, state);
+                    source.setTimeout(timeout == null ? UnsignedInteger.ZERO : timeout);
+                    break;
+                case 4:
+                    source.setDynamic(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 5:
+                    source.setDynamicNodeProperties(state.getDecoder().readMap(buffer, state));
+                    break;
+                case 6:
+                    source.setDistributionMode(state.getDecoder().readSymbol(buffer, state));
+                    break;
+                case 7:
+                    source.setFilter(state.getDecoder().readMap(buffer, state));
+                    break;
+                case 8:
+                    source.setDefaultOutcome(state.getDecoder().readObject(buffer, state, Outcome.class));
+                    break;
+                case 9:
+                    source.setOutcomes(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 10:
+                    source.setCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+            }
+        }
+
+        return source;
+    }
+
+    @Override
+    public Source readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readSource(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Source[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Source[] result = new Source[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readSource(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Source readSource(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Source source = new Source();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_SOURCE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Source list encoding: " + count);
+        }
+
+        if (count > MAX_SOURCE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Source list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    source.setAddress(state.getDecoder().readString(stream, state));
+                    break;
+                case 1:
+                    final long durability = state.getDecoder().readUnsignedInteger(stream, state, 0);
+                    source.setDurable(TerminusDurability.valueOf(durability));
+                    break;
+                case 2:
+                    final Symbol expiryPolicy = state.getDecoder().readSymbol(stream, state);
+                    source.setExpiryPolicy(expiryPolicy == null ? TerminusExpiryPolicy.SESSION_END : TerminusExpiryPolicy.valueOf(expiryPolicy));
+                    break;
+                case 3:
+                    final UnsignedInteger timeout = state.getDecoder().readUnsignedInteger(stream, state);
+                    source.setTimeout(timeout == null ? UnsignedInteger.ZERO : timeout);
+                    break;
+                case 4:
+                    source.setDynamic(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 5:
+                    source.setDynamicNodeProperties(state.getDecoder().readMap(stream, state));
+                    break;
+                case 6:
+                    source.setDistributionMode(state.getDecoder().readSymbol(stream, state));
+                    break;
+                case 7:
+                    source.setFilter(state.getDecoder().readMap(stream, state));
+                    break;
+                case 8:
+                    source.setDefaultOutcome(state.getDecoder().readObject(stream, state, Outcome.class));
+                    break;
+                case 9:
+                    source.setOutcomes(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 10:
+                    source.setCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+            }
+        }
+
+        return source;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/TargetTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/TargetTypeDecoder.java
new file mode 100644
index 0000000..efaaa86
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/messaging/TargetTypeDecoder.java
@@ -0,0 +1,208 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.messaging;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+
+/**
+ * Decoder of AMQP Target type values from a byte stream
+ */
+public final class TargetTypeDecoder extends AbstractDescribedTypeDecoder<Target> {
+
+    private static final int MIN_TARGET_LIST_ENTRIES = 0;
+    private static final int MAX_TARGET_LIST_ENTRIES = 7;
+
+    @Override
+    public Class<Target> getTypeClass() {
+        return Target.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Target.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Target.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Target readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readTarget(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Target[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Target[] result = new Target[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readTarget(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Target readTarget(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Target target = new Target();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_TARGET_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Target list encoding: " + count);
+        }
+
+        if (count > MAX_TARGET_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Target list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    target.setAddress(state.getDecoder().readString(buffer, state));
+                    break;
+                case 1:
+                    final long durability = state.getDecoder().readUnsignedInteger(buffer, state, 0);
+                    target.setDurable(TerminusDurability.valueOf(durability));
+                    break;
+                case 2:
+                    final Symbol expiryPolicy = state.getDecoder().readSymbol(buffer, state);
+                    target.setExpiryPolicy(expiryPolicy == null ? TerminusExpiryPolicy.SESSION_END : TerminusExpiryPolicy.valueOf(expiryPolicy));
+                    break;
+                case 3:
+                    final UnsignedInteger timeout = state.getDecoder().readUnsignedInteger(buffer, state);
+                    target.setTimeout(timeout == null ? UnsignedInteger.ZERO : timeout);
+                    break;
+                case 4:
+                    target.setDynamic(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 5:
+                    target.setDynamicNodeProperties(state.getDecoder().readMap(buffer, state));
+                    break;
+                case 6:
+                    target.setCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+            }
+        }
+
+        return target;
+    }
+
+    @Override
+    public Target readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readTarget(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Target[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Target[] result = new Target[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readTarget(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Target readTarget(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Target target = new Target();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_TARGET_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Target list encoding: " + count);
+        }
+
+        if (count > MAX_TARGET_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Target list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    target.setAddress(state.getDecoder().readString(stream, state));
+                    break;
+                case 1:
+                    final long durability = state.getDecoder().readUnsignedInteger(stream, state, 0);
+                    target.setDurable(TerminusDurability.valueOf(durability));
+                    break;
+                case 2:
+                    final Symbol expiryPolicy = state.getDecoder().readSymbol(stream, state);
+                    target.setExpiryPolicy(expiryPolicy == null ? TerminusExpiryPolicy.SESSION_END : TerminusExpiryPolicy.valueOf(expiryPolicy));
+                    break;
+                case 3:
+                    final UnsignedInteger timeout = state.getDecoder().readUnsignedInteger(stream, state);
+                    target.setTimeout(timeout == null ? UnsignedInteger.ZERO : timeout);
+                    break;
+                case 4:
+                    target.setDynamic(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 5:
+                    target.setDynamicNodeProperties(state.getDecoder().readMap(stream, state));
+                    break;
+                case 6:
+                    target.setCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+            }
+        }
+
+        return target;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractArrayTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractArrayTypeDecoder.java
new file mode 100644
index 0000000..48a5198
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractArrayTypeDecoder.java
@@ -0,0 +1,438 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveArrayTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Base for the decoders of AMQP Array types that defaults to returning opaque Object
+ * values to match what the other decoders do.  External decoding tools will need to use
+ * the {@link PrimitiveArrayTypeDecoder#isArrayType()} checks to determine how they want
+ * to read and return array types.
+ */
+public abstract class AbstractArrayTypeDecoder extends AbstractPrimitiveTypeDecoder<Object> implements PrimitiveArrayTypeDecoder {
+
+    @Override
+    public Class<Object> getTypeClass() {
+        return Object.class;
+    }
+
+    @Override
+    public boolean isArrayType() {
+        return true;
+    }
+
+    @Override
+    public Object readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        int size = readSize(buffer);
+        int count = readCount(buffer);
+
+        if (getTypeCode() == (EncodingCodes.ARRAY32 & 0xff)) {
+            size -= 8; // 4 bytes each for size and count;
+        } else {
+            size -= 2; // 1 byte each for size and count;
+        }
+
+        if (size > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                "Array size indicated %d is greater than the amount of data available to decode (%d)",
+                size, buffer.getReadableBytes()));
+        }
+
+        return decodeArray(buffer, state, count);
+    }
+
+    @Override
+    public Object readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        readSize(stream);
+
+        return decodeAsObject(stream, state, readCount(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(readSize(buffer));
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, readSize(stream));
+    }
+
+    protected abstract int readSize(ProtonBuffer buffer);
+
+    protected abstract int readCount(ProtonBuffer buffer);
+
+    protected abstract int readSize(InputStream stream);
+
+    protected abstract int readCount(InputStream stream);
+
+    private static Object decodeArray(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        if (decoder instanceof PrimitiveTypeDecoder) {
+            final PrimitiveTypeDecoder<?> primitiveTypeDecoder = (PrimitiveTypeDecoder<?>) decoder;
+            final int typeCode = primitiveTypeDecoder.getTypeCode();
+
+            if (primitiveTypeDecoder.isJavaPrimitive()) {
+
+                if (typeCode != EncodingCodes.BOOLEAN_TRUE && typeCode != EncodingCodes.BOOLEAN_FALSE) {
+                    if (count > buffer.getReadableBytes()) {
+                        throw new DecodeException(String.format(
+                            "Array element count %d is specified to be greater than the amount of data available (%d)",
+                            count, buffer.getReadableBytes()));
+                    }
+                }
+
+                final Class<?> typeClass = decoder.getTypeClass();
+
+                if (Boolean.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((BooleanTypeDecoder) decoder, buffer, state, count);
+                } else if (Byte.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((ByteTypeDecoder) decoder, buffer, state, count);
+                } else if (Short.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((ShortTypeDecoder) decoder, buffer, state, count);
+                } else if (Integer.class.equals(typeClass)) {
+                    if (primitiveTypeDecoder.getTypeCode() == (EncodingCodes.INT & 0xff)) {
+                        return decodePrimitiveTypeArray((Integer32TypeDecoder) decoder, buffer, state, count);
+                    } else {
+                        return decodePrimitiveTypeArray((Integer8TypeDecoder) decoder, buffer, state, count);
+                    }
+                } else if (Long.class.equals(typeClass)) {
+                    if (primitiveTypeDecoder.getTypeCode() == (EncodingCodes.LONG & 0xff)) {
+                        return decodePrimitiveTypeArray((LongTypeDecoder) decoder, buffer, state, count);
+                    } else {
+                        return decodePrimitiveTypeArray((Long8TypeDecoder) decoder, buffer, state, count);
+                    }
+                } else if (Double.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((DoubleTypeDecoder) decoder, buffer, state, count);
+                } else if (Float.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((FloatTypeDecoder) decoder, buffer, state, count);
+                } else if (Character.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((CharacterTypeDecoder) decoder, buffer, state, count);
+                } else {
+                    throw new DecodeException("Unexpected class " + decoder.getClass().getName());
+                }
+            } else if (decoder.isArrayType()) {
+                return decodeNonPrimitiveArray(decoder, buffer, state, count);
+            } else {
+                if (typeCode != EncodingCodes.ULONG0 && typeCode != EncodingCodes.UINT0 && typeCode != EncodingCodes.LIST0) {
+                    if (count > buffer.getReadableBytes()) {
+                        throw new DecodeException(String.format(
+                            "Array element count %d is specified to be greater than the amount of data available (%d)",
+                            count, buffer.getReadableBytes()));
+                    }
+                }
+
+                return decoder.readArrayElements(buffer, state, count);
+            }
+        } else {
+            if (count > buffer.getReadableBytes()) {
+                throw new DecodeException(String.format(
+                    "Array element count %d is specified to be greater than the amount of data available (%d)",
+                    count, buffer.getReadableBytes()));
+            }
+
+            return decodeNonPrimitiveArray(decoder, buffer, state, count);
+        }
+    }
+
+    private static Object decodeNonPrimitiveArray(TypeDecoder<?> decoder, ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        if (decoder.isArrayType()) {
+            final PrimitiveArrayTypeDecoder arrayDecoder = (PrimitiveArrayTypeDecoder) decoder;
+
+            final Object[] array = new Object[count];
+            for (int i = 0; i < count; i++) {
+                array[i] = arrayDecoder.readValue(buffer, state);
+            }
+
+            return array;
+        } else {
+            return decoder.readArrayElements(buffer, state, count);
+        }
+    }
+
+    private static boolean[] decodePrimitiveTypeArray(BooleanTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final boolean[] array = new boolean[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static byte[] decodePrimitiveTypeArray(ByteTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final byte[] array = new byte[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static char[] decodePrimitiveTypeArray(CharacterTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final char[] array = new char[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static short[] decodePrimitiveTypeArray(ShortTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final short[] array = new short[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static int[] decodePrimitiveTypeArray(Integer32TypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final int[] array = new int[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static int[] decodePrimitiveTypeArray(Integer8TypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final int[] array = new int[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static long[] decodePrimitiveTypeArray(LongTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final long[] array = new long[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static long[] decodePrimitiveTypeArray(Long8TypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final long[] array = new long[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static float[] decodePrimitiveTypeArray(FloatTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final float[] array = new float[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    private static double[] decodePrimitiveTypeArray(DoubleTypeDecoder decoder, ProtonBuffer buffer, DecoderState state, int count) {
+        final double[] array = new double[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    //----- InputStream based array decoding
+
+    private static Object decodeAsObject(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        if (decoder instanceof PrimitiveTypeDecoder) {
+            final PrimitiveTypeDecoder<?> primitiveTypeDecoder = (PrimitiveTypeDecoder<?>) decoder;
+            if (primitiveTypeDecoder.isJavaPrimitive()) {
+                final Class<?> typeClass = decoder.getTypeClass();
+
+                if (Boolean.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((BooleanTypeDecoder) decoder, stream, state, count);
+                } else if (Byte.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((ByteTypeDecoder) decoder, stream, state, count);
+                } else if (Short.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((ShortTypeDecoder) decoder, stream, state, count);
+                } else if (Integer.class.equals(typeClass)) {
+                    if (primitiveTypeDecoder.getTypeCode() == (EncodingCodes.INT & 0xff)) {
+                        return decodePrimitiveTypeArray((Integer32TypeDecoder) decoder, stream, state, count);
+                    } else {
+                        return decodePrimitiveTypeArray((Integer8TypeDecoder) decoder, stream, state, count);
+                    }
+                } else if (Long.class.equals(typeClass)) {
+                    if (primitiveTypeDecoder.getTypeCode() == (EncodingCodes.LONG & 0xff)) {
+                        return decodePrimitiveTypeArray((LongTypeDecoder) decoder, stream, state, count);
+                    } else {
+                        return decodePrimitiveTypeArray((Long8TypeDecoder) decoder, stream, state, count);
+                    }
+                } else if (Double.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((DoubleTypeDecoder) decoder, stream, state, count);
+                } else if (Float.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((FloatTypeDecoder) decoder, stream, state, count);
+                } else if (Character.class.equals(typeClass)) {
+                    return decodePrimitiveTypeArray((CharacterTypeDecoder) decoder, stream, state, count);
+                } else {
+                    throw new DecodeException("Unexpected class " + decoder.getClass().getName());
+                }
+            }
+        }
+
+        return decodeNonPrimitiveArray(decoder, stream, state, count);
+    }
+
+    private static Object[] decodeNonPrimitiveArray(StreamTypeDecoder<?> decoder, InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        if (decoder.isArrayType()) {
+            final PrimitiveArrayTypeDecoder arrayDecoder = (PrimitiveArrayTypeDecoder) decoder;
+
+            final Object[] array = new Object[count];
+            for (int i = 0; i < count; i++) {
+                array[i] = arrayDecoder.readValue(stream, state);
+            }
+
+            return array;
+        } else {
+            return decoder.readArrayElements(stream, state, count);
+        }
+    }
+
+    private static boolean[] decodePrimitiveTypeArray(BooleanTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final boolean[] array = new boolean[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static byte[] decodePrimitiveTypeArray(ByteTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final byte[] array = new byte[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static char[] decodePrimitiveTypeArray(CharacterTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final char[] array = new char[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static short[] decodePrimitiveTypeArray(ShortTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final short[] array = new short[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static int[] decodePrimitiveTypeArray(Integer32TypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final int[] array = new int[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static int[] decodePrimitiveTypeArray(Integer8TypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final int[] array = new int[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static long[] decodePrimitiveTypeArray(LongTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final long[] array = new long[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static long[] decodePrimitiveTypeArray(Long8TypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final long[] array = new long[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static float[] decodePrimitiveTypeArray(FloatTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final float[] array = new float[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+
+    private static double[] decodePrimitiveTypeArray(DoubleTypeDecoder decoder, InputStream stream, StreamDecoderState state, int count) {
+        final double[] array = new double[count];
+
+        for (int i = 0; i < count; i++) {
+            array[i] = decoder.readPrimitiveValue(stream, state);
+        }
+
+        return array;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractBinaryTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractBinaryTypeDecoder.java
new file mode 100644
index 0000000..db70da1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractBinaryTypeDecoder.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+
+/**
+ * Base class for the various Binary type decoders used to read AMQP Binary values.
+ */
+public abstract class AbstractBinaryTypeDecoder extends AbstractPrimitiveTypeDecoder<Binary> implements BinaryTypeDecoder {
+
+    @Override
+    public Binary readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return new Binary(readValueAsBuffer(buffer, state));
+    }
+
+    public ProtonBuffer readValueAsBuffer(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int length = readSize(buffer);
+
+        if (length > buffer.getReadableBytes()) {
+            throw new DecodeException(
+                String.format("Binary data size %d is specified to be greater than the amount " +
+                              "of data available (%d)", length, buffer.getReadableBytes()));
+        }
+
+        final ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.allocate(length, length);
+
+        buffer.readBytes(payload);
+
+        return payload;
+    }
+
+    public byte[] readValueAsArray(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int length = readSize(buffer);
+
+        if (length > buffer.getReadableBytes()) {
+            throw new DecodeException(
+                String.format("Binary data size %d is specified to be greater than the amount " +
+                              "of data available (%d)", length, buffer.getReadableBytes()));
+        }
+
+        final byte[] payload = new byte[length];
+
+        buffer.readBytes(payload);
+
+        return payload;
+    }
+
+    @Override
+    public Binary readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return new Binary(readValueAsBuffer(stream, state));
+    }
+
+    public ProtonBuffer readValueAsBuffer(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonByteBufferAllocator.DEFAULT.wrap(readValueAsArray(stream, state));
+    }
+
+    public byte[] readValueAsArray(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final int length = readSize(stream);
+        final byte[] payload = new byte[length];
+
+        try {
+            stream.read(payload);
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading Binary payload bytes", ex);
+        }
+
+        return payload;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int length = readSize(buffer);
+
+        if (length > buffer.getReadableBytes()) {
+            throw new DecodeException(
+                String.format("Binary data size %d is specified to be greater than the amount " +
+                              "of data available (%d)", length, buffer.getReadableBytes()));
+        }
+
+        buffer.skipBytes(length);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        try {
+            stream.skip(readSize(stream));
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading Binary payload bytes", ex);
+        }
+    }
+
+    @Override
+    public abstract int readSize(ProtonBuffer buffer);
+
+    @Override
+    public abstract int readSize(InputStream stream);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractListTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractListTypeDecoder.java
new file mode 100644
index 0000000..413afe9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractListTypeDecoder.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+
+/**
+ * Base for the various List type decoders needed to read AMQP List values.
+ */
+@SuppressWarnings("rawtypes")
+public abstract class AbstractListTypeDecoder extends AbstractPrimitiveTypeDecoder<List> implements ListTypeDecoder {
+
+    @Override
+    public List<Object> readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int size = readSize(buffer);
+
+        // Ensure we do not allocate an array of size greater then the available data, otherwise there is a risk for an OOM error
+        if (size > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "List element size %d is specified to be greater than the amount " +
+                    "of data available (%d)", size, buffer.getReadableBytes()));
+        }
+
+        final int count = readCount(buffer);
+
+        if (count > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "Symbol encoded element count %d is specified to be greater than the amount " +
+                    "of data available (%d)", count, buffer.getReadableBytes()));
+        }
+
+        final List<Object> list = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            list.add(state.getDecoder().readObject(buffer, state));
+        }
+
+        return list;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(readSize(buffer));
+    }
+
+    @Override
+    public List<Object> readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        readSize(stream);
+        final int count = readCount(stream);
+
+        final List<Object> list = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            list.add(state.getDecoder().readObject(stream, state));
+        }
+
+        return list;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        try {
+            stream.skip(readSize(stream));
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading List payload bytes", ex);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractMapTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractMapTypeDecoder.java
new file mode 100644
index 0000000..09d3698
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractMapTypeDecoder.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+
+/**
+ * Base for the various Map type decoders used to read AMQP Map values.
+ */
+@SuppressWarnings("rawtypes")
+public abstract class AbstractMapTypeDecoder extends AbstractPrimitiveTypeDecoder<Map> implements MapTypeDecoder {
+
+    @Override
+    public Map<Object, Object> readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int size = readSize(buffer);
+
+        if (size > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "Map encoded size %d is specified to be greater than the amount " +
+                    "of data available (%d)", size, buffer.getReadableBytes()));
+        }
+
+        final int count = readCount(buffer);
+
+        if (count % 2 != 0) {
+            throw new DecodeException(String.format(
+                "Map encoded number of elements %d is not an even number.", count));
+        }
+
+        // Count include both key and value so we must include that in the loop
+        final Map<Object, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            Object key = state.getDecoder().readObject(buffer, state);
+            Object value = state.getDecoder().readObject(buffer, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(readSize(buffer));
+    }
+
+    @Override
+    public Map<Object, Object> readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        readSize(stream);
+        final int count = readCount(stream);
+
+        if (count % 2 != 0) {
+            throw new DecodeException(String.format(
+                "Map encoded number of elements %d is not an even number.", count));
+        }
+
+        // Count include both key and value so we must include that in the loop
+        final Map<Object, Object> map = new LinkedHashMap<>(count);
+        for (int i = 0; i < count / 2; i++) {
+            Object key = state.getDecoder().readObject(stream, state);
+            Object value = state.getDecoder().readObject(stream, state);
+
+            map.put(key, value);
+        }
+
+        return map;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        try {
+            stream.skip(readSize(stream));
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading Map payload bytes", ex);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractStringTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractStringTypeDecoder.java
new file mode 100644
index 0000000..a293213
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractStringTypeDecoder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+
+/**
+ * Base for the various String type Decoders used to read AMQP String values.
+ */
+public abstract class AbstractStringTypeDecoder extends AbstractPrimitiveTypeDecoder<String> implements StringTypeDecoder {
+
+    @Override
+    public String readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int length = readSize(buffer);
+
+        if (length > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "String encoded size %d is specified to be greater than the amount " +
+                    "of data available (%d)", length, buffer.getReadableBytes()));
+        }
+
+        if (length != 0) {
+            return state.decodeUTF8(buffer, length);
+        } else {
+            return "";
+        }
+    }
+
+    @Override
+    public String readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final int length = readSize(stream);
+
+        if (length != 0) {
+            return state.decodeUTF8(stream, length);
+        } else {
+            return "";
+        }
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(readSize(buffer));
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        try {
+            stream.skip(readSize(stream));
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading String payload bytes", ex);
+        }
+    }
+
+    protected abstract int readSize(ProtonBuffer buffer);
+
+    protected abstract int readSize(InputStream stream);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractSymbolTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractSymbolTypeDecoder.java
new file mode 100644
index 0000000..0c89540
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/AbstractSymbolTypeDecoder.java
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Base class for the Symbol decoders used on AMQP Symbol types.
+ */
+public abstract class AbstractSymbolTypeDecoder extends AbstractPrimitiveTypeDecoder<Symbol> implements SymbolTypeDecoder {
+
+    @Override
+    public Symbol readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final int length = readSize(buffer);
+
+        if (length == 0) {
+            return Symbol.valueOf("");
+        }
+
+        if (length > buffer.getReadableBytes()) {
+            throw new DecodeException(String.format(
+                    "Symbol encoded size %d is specified to be greater than the amount " +
+                    "of data available (%d)", length, buffer.getReadableBytes()));
+        }
+
+        final ProtonBuffer symbolBuffer = buffer.slice(buffer.getReadIndex(), length);
+        buffer.skipBytes(length);
+
+        return Symbol.getSymbol(symbolBuffer, true);
+    }
+
+    /**
+     * Reads a String view of an encoded Symbol value from the given buffer.
+     * <p>
+     * This method has the same result as calling the Symbol reading variant
+     * {@link #readValue(ProtonBuffer, DecoderState)} and then invoking the toString
+     * method on the resulting Symbol.
+     *
+     * @param buffer
+     *      The buffer to read the encoded symbol from.
+     * @param state
+     *      The encoder state that applied to this decode operation.
+     *
+     * @return a String view of the encoded Symbol value.
+     *
+     * @throws DecodeException if an error occurs decoding the Symbol from the given buffer.
+     */
+    public String readString(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return readValue(buffer, state).toString();
+    }
+
+    @Override
+    public Symbol readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final int length = readSize(stream);
+
+        if (length == 0) {
+            return Symbol.valueOf("");
+        }
+
+        final byte[] symbolBytes = new byte[length];
+
+        try {
+            stream.read(symbolBytes);
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading Symbol payload bytes", ex);
+        }
+
+        return Symbol.getSymbol(ProtonByteBufferAllocator.DEFAULT.wrap(symbolBytes), true);
+    }
+
+    /**
+     * Reads a String view of an encoded Symbol value from the given buffer.
+     * <p>
+     * This method has the same result as calling the Symbol reading variant
+     * {@link #readValue(ProtonBuffer, DecoderState)} and then invoking the toString
+     * method on the resulting Symbol.
+     *
+     * @param stream
+     *      The InputStream to read the encoded symbol from.
+     * @param state
+     *      The encoder state that applied to this decode operation.
+     *
+     * @return a String view of the encoded Symbol value.
+     *
+     * @throws DecodeException if an error occurs decoding the Symbol from the given buffer.
+     */
+    public String readString(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return readValue(stream, state).toString();
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(readSize(buffer));
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        try {
+            stream.skip(readSize(stream));
+        } catch (IOException ex) {
+            throw new DecodeException("Error while reading Symbol payload bytes", ex);
+        }
+    }
+
+    /**
+     * Subclasses must read the correct number of bytes from the buffer to determine the
+     * size of the encoded Symbol value.
+     *
+     * @param buffer
+     *      The buffer to read the size from.
+     *
+     * @return the number of bytes that make up the encoded Symbol value.
+     *
+     * @throws DecodeException if an error occurs reading the size value.
+     */
+    protected abstract int readSize(ProtonBuffer buffer) throws DecodeException;
+
+    /**
+     * Subclasses must read the correct number of bytes from the buffer to determine the
+     * size of the encoded Symbol value.
+     *
+     * @param stream
+     *      The InputStream to read the size from.
+     *
+     * @return the number of bytes that make up the encoded Symbol value.
+     *
+     * @throws DecodeException if an error occurs reading the size value.
+     */
+    protected abstract int readSize(InputStream stream) throws DecodeException;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Array32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Array32TypeDecoder.java
new file mode 100644
index 0000000..006c23a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Array32TypeDecoder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Arrays from a byte stream.
+ */
+public final class Array32TypeDecoder extends AbstractArrayTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.ARRAY32 & 0xff;
+    }
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return false;
+    }
+
+    @Override
+    protected int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    protected int readCount(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    protected int readSize(InputStream stream) {
+        return ProtonStreamUtils.readInt(stream);
+    }
+
+    @Override
+    protected int readCount(InputStream stream) {
+        return ProtonStreamUtils.readInt(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Array8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Array8TypeDecoder.java
new file mode 100644
index 0000000..d6a8f68
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Array8TypeDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Arrays from a byte stream.
+ */
+public final class Array8TypeDecoder extends AbstractArrayTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.ARRAY8 & 0xff;
+    }
+
+    @Override
+    protected int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    protected int readCount(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    protected int readSize(InputStream stream) {
+        return ProtonStreamUtils.readByte(stream) & 0xff;
+    }
+
+    @Override
+    protected int readCount(InputStream stream) {
+        return ProtonStreamUtils.readByte(stream) & 0xff;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Binary32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Binary32TypeDecoder.java
new file mode 100644
index 0000000..a78bceb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Binary32TypeDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Binary values from a byte stream.
+ */
+public final class Binary32TypeDecoder extends AbstractBinaryTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.VBIN32 & 0xff;
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    public int readSize(InputStream stream) {
+        return ProtonStreamUtils.readInt(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Binary8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Binary8TypeDecoder.java
new file mode 100644
index 0000000..d70bd8b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Binary8TypeDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Binary values with length less than 255 from a byte stream.
+ */
+public final class Binary8TypeDecoder extends AbstractBinaryTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.VBIN8 & 0xff;
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public int readSize(InputStream stream) {
+        return ProtonStreamUtils.readByte(stream) & 0xff;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BinaryTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BinaryTypeDecoder.java
new file mode 100644
index 0000000..6f431fc
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BinaryTypeDecoder.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+
+/**
+ * Base for all Binary type value decoders.
+ */
+public interface BinaryTypeDecoder extends PrimitiveTypeDecoder<Binary> {
+
+    @Override
+    default Class<Binary> getTypeClass() {
+        return Binary.class;
+    }
+
+    /**
+     * Reads the encoded size value for the encoded binary payload and returns it.  The
+     * read is destructive and the {@link TypeDecoder} read methods cannot be called after
+     * this unless the {@link ProtonBuffer} is reset via a position marker.  This method can
+     * be useful when the caller intends to manually read the binary payload from the given
+     * {@link ProtonBuffer}.
+     *
+     * @param buffer
+     *      the buffer from which the binary encoded size should be read.
+     *
+     * @return the size of the binary payload that is encoded in the given {@link ProtonBuffer}.
+     *
+     * @throws DecodeException if an error occurs while reading the binary size.
+     */
+    int readSize(ProtonBuffer buffer) throws DecodeException;
+
+    /**
+     * Reads the encoded size value for the encoded binary payload and returns it.  The
+     * read is destructive and the {@link TypeDecoder} read methods cannot be called after
+     * this unless the {@link InputStream} is reset via a position marker.  This method can
+     * be useful when the caller intends to manually read the binary payload from the given
+     * {@link InputStream}.
+     *
+     * @param stream
+     *      the stream from which the binary encoded size should be read.
+     *
+     * @return the size of the binary payload that is encoded in the given {@link InputStream}.
+     *
+     * @throws DecodeException if an error occurs while reading the binary size.
+     */
+    int readSize(InputStream stream) throws DecodeException;
+
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanFalseTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanFalseTypeDecoder.java
new file mode 100644
index 0000000..1c2c905
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanFalseTypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+
+/**
+ * Decoder of AMQP Boolean False values from a byte stream.
+ */
+public final class BooleanFalseTypeDecoder extends BooleanTypeDecoder {
+
+    @Override
+    public Boolean readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return Boolean.FALSE;
+    }
+
+    @Override
+    public Boolean readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Boolean.FALSE;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.BOOLEAN_FALSE & 0xff;
+    }
+
+    @Override
+    public boolean readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return false;
+    }
+
+    @Override
+    public boolean readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return false;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanTrueTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanTrueTypeDecoder.java
new file mode 100644
index 0000000..e9950f1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanTrueTypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+
+/**
+ * Decoder of AMQP Boolean True values from a byte stream.
+ */
+public final class BooleanTrueTypeDecoder extends BooleanTypeDecoder {
+
+    @Override
+    public Boolean readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return Boolean.TRUE;
+    }
+
+    @Override
+    public Boolean readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Boolean.TRUE;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.BOOLEAN_TRUE & 0xff;
+    }
+
+    @Override
+    public boolean readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return true;
+    }
+
+    @Override
+    public boolean readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return true;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanTypeDecoder.java
new file mode 100644
index 0000000..4824258
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/BooleanTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Boolean values from a byte stream.
+ */
+public class BooleanTypeDecoder extends AbstractPrimitiveTypeDecoder<Boolean> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Boolean> getTypeClass() {
+        return Boolean.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.BOOLEAN & 0xff;
+    }
+
+    @Override
+    public Boolean readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte() == 0 ? Boolean.FALSE : Boolean.TRUE;
+    }
+
+    @Override
+    public Boolean readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream) == 0 ? Boolean.FALSE : Boolean.TRUE;
+    }
+
+    public boolean readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte() == 0 ? false : true;
+    }
+
+    public boolean readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream) == 0 ? false : true;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.readByte();
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.readByte(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ByteTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ByteTypeDecoder.java
new file mode 100644
index 0000000..396b3d3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ByteTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Bytes from a byte stream.
+ */
+public final class ByteTypeDecoder extends AbstractPrimitiveTypeDecoder<Byte> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Byte> getTypeClass() {
+        return Byte.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.BYTE & 0xff;
+    }
+
+    @Override
+    public Byte readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte();
+    }
+
+    @Override
+    public Byte readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+
+    public byte readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte();
+    }
+
+    public byte readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Byte.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Byte.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/CharacterTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/CharacterTypeDecoder.java
new file mode 100644
index 0000000..3c775ee
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/CharacterTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Character from a byte stream.
+ */
+public final class CharacterTypeDecoder extends AbstractPrimitiveTypeDecoder<Character> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Character> getTypeClass() {
+        return Character.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.CHAR & 0xff;
+    }
+
+    @Override
+    public Character readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return Character.valueOf((char) (buffer.readInt() & 0xffff));
+    }
+
+    @Override
+    public Character readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Character.valueOf((char) ProtonStreamUtils.readInt(stream));
+    }
+
+    public char readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return (char) (buffer.readInt() & 0xffff);
+    }
+
+    public char readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return (char) ProtonStreamUtils.readInt(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Integer.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Integer.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal128TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal128TypeDecoder.java
new file mode 100644
index 0000000..d04fca7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal128TypeDecoder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.Decimal128;
+
+/**
+ * Decoder of AMQP Decimal128 values from a byte stream
+ */
+public final class Decimal128TypeDecoder extends AbstractPrimitiveTypeDecoder<Decimal128> {
+
+    @Override
+    public Class<Decimal128> getTypeClass() {
+        return Decimal128.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.DECIMAL128 & 0xff;
+    }
+
+    @Override
+    public Decimal128 readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        long msb = buffer.readLong();
+        long lsb = buffer.readLong();
+
+        return new Decimal128(msb, lsb);
+    }
+
+    @Override
+    public Decimal128 readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        long msb = ProtonStreamUtils.readLong(stream);
+        long lsb = ProtonStreamUtils.readLong(stream);
+
+        return new Decimal128(msb, lsb);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Decimal128.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Decimal128.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal32TypeDecoder.java
new file mode 100644
index 0000000..f0a42b6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal32TypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.Decimal32;
+
+/**
+ * Decoder of AMQP Decimal32 values from a byte stream
+ */
+public final class Decimal32TypeDecoder extends AbstractPrimitiveTypeDecoder<Decimal32> {
+
+    @Override
+    public Class<Decimal32> getTypeClass() {
+        return Decimal32.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.DECIMAL32 & 0xff;
+    }
+
+    @Override
+    public Decimal32 readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException{
+        return new Decimal32(buffer.readInt());
+    }
+
+    @Override
+    public Decimal32 readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return new Decimal32(ProtonStreamUtils.readInt(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Decimal32.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Decimal32.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal64TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal64TypeDecoder.java
new file mode 100644
index 0000000..60d5466
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Decimal64TypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.Decimal64;
+
+/**
+ * Decoder of AMQP Decimal64 values from a byte stream
+ */
+public final class Decimal64TypeDecoder extends AbstractPrimitiveTypeDecoder<Decimal64> {
+
+    @Override
+    public Class<Decimal64> getTypeClass() {
+        return Decimal64.class;
+    }
+
+    @Override
+    public Decimal64 readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return new Decimal64(buffer.readLong());
+    }
+
+    @Override
+    public Decimal64 readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return new Decimal64(ProtonStreamUtils.readLong(stream));
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.DECIMAL64 & 0xff;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Decimal64.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Decimal64.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/DoubleTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/DoubleTypeDecoder.java
new file mode 100644
index 0000000..759e065
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/DoubleTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Double values from a byte stream
+ */
+public final class DoubleTypeDecoder extends AbstractPrimitiveTypeDecoder<Double> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Double> getTypeClass() {
+        return Double.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.DOUBLE & 0xff;
+    }
+
+    @Override
+    public Double readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readDouble();
+    }
+
+    @Override
+    public Double readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readDouble(stream);
+    }
+
+    public double readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readDouble();
+    }
+
+    public double readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readDouble(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Double.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Double.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/FloatTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/FloatTypeDecoder.java
new file mode 100644
index 0000000..71c8e5f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/FloatTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Float values from a byte stream.
+ */
+public final class FloatTypeDecoder extends AbstractPrimitiveTypeDecoder<Float> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Float> getTypeClass() {
+        return Float.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.FLOAT & 0xff;
+    }
+
+    @Override
+    public Float readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readFloat();
+    }
+
+    @Override
+    public Float readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readFloat(stream);
+    }
+
+    public float readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readFloat();
+    }
+
+    public float readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readFloat(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Float.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Float.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Integer32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Integer32TypeDecoder.java
new file mode 100644
index 0000000..12a2684
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Integer32TypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decode AMQP Integer values from a byte stream
+ */
+public final class Integer32TypeDecoder extends AbstractPrimitiveTypeDecoder<Integer> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Integer> getTypeClass() {
+        return Integer.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.INT & 0xff;
+    }
+
+    @Override
+    public Integer readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    public Integer readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+
+    public int readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    public int readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Integer.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Integer.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Integer8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Integer8TypeDecoder.java
new file mode 100644
index 0000000..4ae4aec
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Integer8TypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decode AMQP small Integer values from a byte stream
+ */
+public final class Integer8TypeDecoder extends AbstractPrimitiveTypeDecoder<Integer> {
+
+    @Override
+    public Class<Integer> getTypeClass() {
+        return Integer.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SMALLINT & 0xff;
+    }
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    public int readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte();
+    }
+
+    public int readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+
+    @Override
+    public Integer readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public Integer readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Integer.valueOf(ProtonStreamUtils.readByte(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Byte.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Byte.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List0TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List0TypeDecoder.java
new file mode 100644
index 0000000..623230f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List0TypeDecoder.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+
+/**
+ * Decoder of Zero sized AMQP List values from a byte stream.
+ */
+@SuppressWarnings( { "unchecked", "rawtypes" } )
+public final class List0TypeDecoder extends AbstractPrimitiveTypeDecoder<List> implements ListTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.LIST0 & 0xff;
+    }
+
+    @Override
+    public List<Object> readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<Object> readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return 0;
+    }
+
+    @Override
+    public int readCount(ProtonBuffer buffer) throws DecodeException {
+        return 0;
+    }
+
+    @Override
+    public int readSize(InputStream stream) throws DecodeException {
+        return 0;
+    }
+
+    @Override
+    public int readCount(InputStream stream) throws DecodeException {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List32TypeDecoder.java
new file mode 100644
index 0000000..ae779ae
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List32TypeDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP List values from a byte stream
+ */
+public final class List32TypeDecoder extends AbstractListTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.LIST32 & 0xff;
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    public int readCount(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    public int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+
+    @Override
+    public int readCount(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List8TypeDecoder.java
new file mode 100644
index 0000000..3bae55e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/List8TypeDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP small List values from a byte stream.
+ */
+public final class List8TypeDecoder extends AbstractListTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.LIST8 & 0xff;
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public int readCount(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+
+    @Override
+    public int readCount(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ListTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ListTypeDecoder.java
new file mode 100644
index 0000000..3c43dae
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ListTypeDecoder.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+
+/**
+ * Base class for List type decoders.
+ */
+@SuppressWarnings("rawtypes")
+public interface ListTypeDecoder extends PrimitiveTypeDecoder<List> {
+
+    int readSize(ProtonBuffer buffer) throws DecodeException;
+
+    int readCount(ProtonBuffer buffer) throws DecodeException;
+
+    int readSize(InputStream stream) throws DecodeException;
+
+    int readCount(InputStream stream) throws DecodeException;
+
+    @Override
+    default Class<List> getTypeClass() {
+        return List.class;
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Long8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Long8TypeDecoder.java
new file mode 100644
index 0000000..76bbf2d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Long8TypeDecoder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decode AMQP small Long values from a byte stream
+ */
+public final class Long8TypeDecoder extends LongTypeDecoder {
+
+    @Override
+    public Long readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return (long) buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public Long readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Long.valueOf(ProtonStreamUtils.readByte(stream));
+    }
+
+    @Override
+    public long readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readByte();
+    }
+
+    @Override
+    public long readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SMALLLONG & 0xff;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Byte.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Byte.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/LongTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/LongTypeDecoder.java
new file mode 100644
index 0000000..3226068
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/LongTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decode AMQP Long values from a byte stream
+ */
+public class LongTypeDecoder extends AbstractPrimitiveTypeDecoder<Long> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Long> getTypeClass() {
+        return Long.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.LONG & 0xff;
+    }
+
+    @Override
+    public Long readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readLong();
+    }
+
+    @Override
+    public Long readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Long.valueOf(ProtonStreamUtils.readLong(stream));
+    }
+
+    public long readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readLong();
+    }
+
+    public long readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readLong(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Long.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Long.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Map32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Map32TypeDecoder.java
new file mode 100644
index 0000000..b529a0f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Map32TypeDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Map value from a byte stream
+ */
+public final class Map32TypeDecoder extends AbstractMapTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.MAP32 & 0xff;
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    public int readCount(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    public int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+
+    @Override
+    public int readCount(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Map8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Map8TypeDecoder.java
new file mode 100644
index 0000000..25b39e1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Map8TypeDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP small Map types from a byte stream
+ */
+public final class Map8TypeDecoder extends AbstractMapTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.MAP8 & 0xff;
+    }
+
+    @Override
+    public int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public int readCount(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    public int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+
+    @Override
+    public int readCount(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/MapTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/MapTypeDecoder.java
new file mode 100644
index 0000000..1dc85a6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/MapTypeDecoder.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+
+/**
+ * Base interface for all AMQP Map type value decoders.
+ */
+@SuppressWarnings("rawtypes")
+public interface MapTypeDecoder extends PrimitiveTypeDecoder<Map> {
+
+    @Override
+    default Class<Map> getTypeClass() {
+        return Map.class;
+    }
+
+    /**
+     * Reads the encoded size of the underlying Map type.
+     *
+     * @param buffer
+     *      The buffer containing the encoded Map type.
+     *
+     * @return the size in bytes of the encoded Map.
+     *
+     * @throws DecodeException if an error occurs reading the value
+     */
+    int readSize(ProtonBuffer buffer) throws DecodeException;
+
+    /**
+     * Reads the count of entries in the encoded Map.
+     * <p>
+     * This value is the total count of all key values pairs, and should
+     * always be an even number as Map types cannot be unbalanced.
+     *
+     * @param buffer
+     *      The buffer containing the encoded Map type.
+     *
+     * @return the number of elements that we encoded from the original Map.
+     *
+     * @throws DecodeException if an error occurs reading the value
+     */
+    int readCount(ProtonBuffer buffer) throws DecodeException;
+
+    /**
+     * Reads the encoded size of the underlying Map type.
+     *
+     * @param stream
+     *      The InputStream containing the encoded Map type.
+     *
+     * @return the size in bytes of the encoded Map.
+     *
+     * @throws DecodeException if an error occurs reading the value
+     */
+    int readSize(InputStream stream) throws DecodeException;
+
+    /**
+     * Reads the count of entries in the encoded Map.
+     * <p>
+     * This value is the total count of all key values pairs, and should
+     * always be an even number as Map types cannot be unbalanced.
+     *
+     * @param stream
+     *      The InputStream containing the encoded Map type.
+     *
+     * @return the number of elements that we encoded from the original Map.
+     *
+     * @throws DecodeException if an error occurs reading the value
+     */
+    int readCount(InputStream stream) throws DecodeException;
+
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/NullTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/NullTypeDecoder.java
new file mode 100644
index 0000000..057c100
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/NullTypeDecoder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+
+/**
+ * Decoder of AMQP Null values from a byte stream.
+ */
+public final class NullTypeDecoder extends AbstractPrimitiveTypeDecoder<Void> {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.NULL & 0xff;
+    }
+
+    @Override
+    public Class<Void> getTypeClass() {
+        return Void.class;
+    }
+
+    @Override
+    public Void readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return null;
+    }
+
+    @Override
+    public Void readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return null;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ShortTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ShortTypeDecoder.java
new file mode 100644
index 0000000..d0b39bd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/ShortTypeDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decode AMQP Short values from a byte stream
+ */
+public final class ShortTypeDecoder extends AbstractPrimitiveTypeDecoder<Short> {
+
+    @Override
+    public boolean isJavaPrimitive() {
+        return true;
+    }
+
+    @Override
+    public Class<Short> getTypeClass() {
+        return Short.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SHORT & 0xff;
+    }
+
+    @Override
+    public Short readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readShort();
+    }
+
+    @Override
+    public Short readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Short.valueOf(ProtonStreamUtils.readShort(stream));
+    }
+
+    public short readPrimitiveValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readShort();
+    }
+
+    public short readPrimitiveValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return ProtonStreamUtils.readShort(stream);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Short.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Short.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/String32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/String32TypeDecoder.java
new file mode 100644
index 0000000..7d130e1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/String32TypeDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP String values from a byte stream.
+ */
+public final class String32TypeDecoder extends AbstractStringTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.STR32 & 0xff;
+    }
+
+    @Override
+    protected int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    protected int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/String8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/String8TypeDecoder.java
new file mode 100644
index 0000000..80544b9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/String8TypeDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP small String values from a byte stream.
+ */
+public final class String8TypeDecoder extends AbstractStringTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.STR8 & 0xff;
+    }
+
+    @Override
+    protected int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    protected int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/StringTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/StringTypeDecoder.java
new file mode 100644
index 0000000..9205694
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/StringTypeDecoder.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+
+/**
+ * Base for all String type decoders
+ */
+public interface StringTypeDecoder extends PrimitiveTypeDecoder<String>{
+
+    @Override
+    default Class<String> getTypeClass() {
+        return String.class;
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Symbol32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Symbol32TypeDecoder.java
new file mode 100644
index 0000000..0653d2b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Symbol32TypeDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Symbol values from a byte stream.
+ */
+public final class Symbol32TypeDecoder extends AbstractSymbolTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SYM32 & 0xff;
+    }
+
+    @Override
+    protected int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readInt();
+    }
+
+    @Override
+    protected int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readInt(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Symbol8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Symbol8TypeDecoder.java
new file mode 100644
index 0000000..168d563
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/Symbol8TypeDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Symbol values from a byte stream.
+ */
+public final class Symbol8TypeDecoder extends AbstractSymbolTypeDecoder {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SYM8 & 0xff;
+    }
+
+    @Override
+    protected int readSize(ProtonBuffer buffer) throws DecodeException {
+        return buffer.readByte() & 0xff;
+    }
+
+    @Override
+    protected int readSize(InputStream stream) throws DecodeException {
+        return ProtonStreamUtils.readByte(stream);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/SymbolTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/SymbolTypeDecoder.java
new file mode 100644
index 0000000..fef8c3a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/SymbolTypeDecoder.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Base for all Symbol type decoders.
+ */
+public interface SymbolTypeDecoder extends PrimitiveTypeDecoder<Symbol>{
+
+    @Override
+    default Class<Symbol> getTypeClass() {
+        return Symbol.class;
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/TimestampTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/TimestampTypeDecoder.java
new file mode 100644
index 0000000..5da4c11
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/TimestampTypeDecoder.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP Timestamp values from a byte stream.
+ */
+public final class TimestampTypeDecoder extends AbstractPrimitiveTypeDecoder<Long> {
+
+    @Override
+    public Class<Long> getTypeClass() {
+        return Long.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.TIMESTAMP & 0xff;
+    }
+
+    @Override
+    public Long readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return buffer.readLong();
+    }
+
+    @Override
+    public Long readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return Long.valueOf(ProtonStreamUtils.readLong(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Long.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Long.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UUIDTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UUIDTypeDecoder.java
new file mode 100644
index 0000000..4658cb8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UUIDTypeDecoder.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+
+/**
+ * Decoder of AMQP UUID values from a byte stream
+ */
+public final class UUIDTypeDecoder extends AbstractPrimitiveTypeDecoder<UUID> {
+
+    private static final int BYTES = Long.BYTES * 2;
+
+    @Override
+    public Class<UUID> getTypeClass() {
+        return UUID.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.UUID & 0xff;
+    }
+
+    @Override
+    public UUID readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        long msb = buffer.readLong();
+        long lsb = buffer.readLong();
+
+        return new UUID(msb, lsb);
+    }
+
+    @Override
+    public UUID readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        long msb = ProtonStreamUtils.readLong(stream);
+        long lsb = ProtonStreamUtils.readLong(stream);
+
+        return new UUID(msb, lsb);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedByteTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedByteTypeDecoder.java
new file mode 100644
index 0000000..98c8326
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedByteTypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+
+/**
+ * Decode AMQP Unsigned Byte values from a byte stream
+ */
+public final class UnsignedByteTypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedByte> {
+
+    @Override
+    public Class<UnsignedByte> getTypeClass() {
+        return UnsignedByte.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.UBYTE & 0xff;
+    }
+
+    @Override
+    public UnsignedByte readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedByte.valueOf(buffer.readByte());
+    }
+
+    @Override
+    public UnsignedByte readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedByte.valueOf(ProtonStreamUtils.readByte(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Byte.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Byte.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger0TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger0TypeDecoder.java
new file mode 100644
index 0000000..6b4fa51
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger0TypeDecoder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Decode AMQP Zero value Unsigned Integer values from a byte stream
+ */
+public final class UnsignedInteger0TypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedInteger> {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.UINT0 & 0xff;
+    }
+
+    @Override
+    public Class<UnsignedInteger> getTypeClass() {
+        return UnsignedInteger.class;
+    }
+
+    @Override
+    public UnsignedInteger readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedInteger.ZERO;
+    }
+
+    @Override
+    public UnsignedInteger readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedInteger.ZERO;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger32TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger32TypeDecoder.java
new file mode 100644
index 0000000..7db7cf7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger32TypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Decode AMQP Unsigned Integer values from a byte stream
+ */
+public final class UnsignedInteger32TypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedInteger> {
+
+    @Override
+    public Class<UnsignedInteger> getTypeClass() {
+        return UnsignedInteger.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.UINT & 0xff;
+    }
+
+    @Override
+    public UnsignedInteger readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedInteger.valueOf((buffer.readInt()));
+    }
+
+    @Override
+    public UnsignedInteger readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedInteger.valueOf(ProtonStreamUtils.readInt(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Integer.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Integer.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger8TypeDecoder.java
new file mode 100644
index 0000000..9f81a02
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedInteger8TypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Decode AMQP Small Unsigned Integer values from a byte stream
+ */
+public class UnsignedInteger8TypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedInteger> {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SMALLUINT & 0xff;
+    }
+
+    @Override
+    public Class<UnsignedInteger> getTypeClass() {
+        return UnsignedInteger.class;
+    }
+
+    @Override
+    public UnsignedInteger readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedInteger.valueOf((buffer.readByte()) & 0xff);
+    }
+
+    @Override
+    public UnsignedInteger readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedInteger.valueOf(ProtonStreamUtils.readByte(stream) & 0xff);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Byte.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Byte.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong0TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong0TypeDecoder.java
new file mode 100644
index 0000000..240e643
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong0TypeDecoder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Decode AMQP Zero value Unsigned Long values from a byte stream
+ */
+public final class UnsignedLong0TypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedLong> {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.ULONG0 & 0xff;
+    }
+
+    @Override
+    public Class<UnsignedLong> getTypeClass() {
+        return UnsignedLong.class;
+    }
+
+    @Override
+    public UnsignedLong readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedLong.ZERO;
+    }
+
+    @Override
+    public UnsignedLong readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedLong.ZERO;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong64TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong64TypeDecoder.java
new file mode 100644
index 0000000..3bce714
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong64TypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Decode AMQP Unsigned Long values from a byte stream
+ */
+public final class UnsignedLong64TypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedLong> {
+
+    @Override
+    public Class<UnsignedLong> getTypeClass() {
+        return UnsignedLong.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.ULONG & 0xff;
+    }
+
+    @Override
+    public UnsignedLong readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedLong.valueOf((buffer.readLong()));
+    }
+
+    @Override
+    public UnsignedLong readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedLong.valueOf(ProtonStreamUtils.readLong(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Long.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Long.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong8TypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong8TypeDecoder.java
new file mode 100644
index 0000000..f8432f8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedLong8TypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Decode AMQP Unsigned small Long values from a byte stream
+ */
+public final class UnsignedLong8TypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedLong> {
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.SMALLULONG & 0xff;
+    }
+
+    @Override
+    public Class<UnsignedLong> getTypeClass() {
+        return UnsignedLong.class;
+    }
+
+    @Override
+    public UnsignedLong readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedLong.valueOf((buffer.readByte() & 0xff));
+    }
+
+    @Override
+    public UnsignedLong readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedLong.valueOf(ProtonStreamUtils.readByte(stream) & 0xff);
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Byte.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Byte.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedShortTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedShortTypeDecoder.java
new file mode 100644
index 0000000..5c73498
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/primitives/UnsignedShortTypeDecoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.primitives;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractPrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * Decode AMQP Unsigned Short values from a byte stream
+ */
+public final class UnsignedShortTypeDecoder extends AbstractPrimitiveTypeDecoder<UnsignedShort> {
+
+    @Override
+    public Class<UnsignedShort> getTypeClass() {
+        return UnsignedShort.class;
+    }
+
+    @Override
+    public int getTypeCode() {
+        return EncodingCodes.USHORT & 0xff;
+    }
+
+    @Override
+    public UnsignedShort readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        return UnsignedShort.valueOf(buffer.readShort());
+    }
+
+    @Override
+    public UnsignedShort readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        return UnsignedShort.valueOf(ProtonStreamUtils.readShort(stream));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        buffer.skipBytes(Short.BYTES);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        ProtonStreamUtils.skipBytes(stream, Short.BYTES);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslChallengeTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslChallengeTypeDecoder.java
new file mode 100644
index 0000000..39268dc
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslChallengeTypeDecoder.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.security;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslChallenge;
+
+/**
+ * Decoder of AMQP SaslChallenge type values from a byte stream.
+ */
+public final class SaslChallengeTypeDecoder extends AbstractDescribedTypeDecoder<SaslChallenge> {
+
+    private static final int REQUIRED_LIST_ENTRIES = 1;
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslChallenge.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslChallenge.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<SaslChallenge> getTypeClass() {
+        return SaslChallenge.class;
+    }
+
+    @Override
+    public SaslChallenge readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    @Override
+    public SaslChallenge[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final SaslChallenge[] result = new SaslChallenge[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    private SaslChallenge readProperties(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslChallenge challenge = new SaslChallenge();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count != REQUIRED_LIST_ENTRIES) {
+            throw new DecodeException("SASL Challenge must contain a single challenge binary: " + count);
+        } else {
+            challenge.setChallenge(state.getDecoder().readBinaryAsBuffer(buffer, state));
+        }
+
+        return challenge;
+    }
+
+    @Override
+    public SaslChallenge readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    @Override
+    public SaslChallenge[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final SaslChallenge[] result = new SaslChallenge[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    private SaslChallenge readProperties(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslChallenge challenge = new SaslChallenge();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count != REQUIRED_LIST_ENTRIES) {
+            throw new DecodeException("SASL Challenge must contain a single challenge binary: " + count);
+        } else {
+            challenge.setChallenge(state.getDecoder().readBinaryAsBuffer(stream, state));
+        }
+
+        return challenge;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslInitTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslInitTypeDecoder.java
new file mode 100644
index 0000000..36526f3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslInitTypeDecoder.java
@@ -0,0 +1,177 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.security;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+
+/**
+ * Decoder of AMQP SaslInit type values from a byte stream.
+ */
+public final class SaslInitTypeDecoder extends AbstractDescribedTypeDecoder<SaslInit> {
+
+    private static final int MIN_SASL_INIT_LIST_ENTRIES = 0;
+    private static final int MAX_SASL_INIT_LIST_ENTRIES = 3;
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslInit.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslInit.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<SaslInit> getTypeClass() {
+        return SaslInit.class;
+    }
+
+    @Override
+    public SaslInit readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslInit[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final SaslInit[] result = new SaslInit[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private SaslInit readProperties(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslInit init = new SaslInit();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_SASL_INIT_LIST_ENTRIES) {
+            throw new DecodeException("Not enougn entries in SaslInit list encoding: " + count);
+        }
+
+        if (count > MAX_SASL_INIT_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in SaslInit list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    init.setMechanism(state.getDecoder().readSymbol(buffer, state));
+                    break;
+                case 1:
+                    init.setInitialResponse(state.getDecoder().readBinaryAsBuffer(buffer, state));
+                    break;
+                case 2:
+                    init.setHostname(state.getDecoder().readString(buffer, state));
+                    break;
+            }
+        }
+
+        return init;
+    }
+
+    @Override
+    public SaslInit readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslInit[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final SaslInit[] result = new SaslInit[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private SaslInit readProperties(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslInit init = new SaslInit();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_SASL_INIT_LIST_ENTRIES) {
+            throw new DecodeException("Not enougn entries in SaslInit list encoding: " + count);
+        }
+
+        if (count > MAX_SASL_INIT_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in SaslInit list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    init.setMechanism(state.getDecoder().readSymbol(stream, state));
+                    break;
+                case 1:
+                    init.setInitialResponse(state.getDecoder().readBinaryAsBuffer(stream, state));
+                    break;
+                case 2:
+                    init.setHostname(state.getDecoder().readString(stream, state));
+                    break;
+            }
+        }
+
+        return init;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslMechanismsTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslMechanismsTypeDecoder.java
new file mode 100644
index 0000000..0866712
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslMechanismsTypeDecoder.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.security;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslMechanisms;
+
+/**
+ * Decoder of AMQP SaslChallenge type values from a byte stream.
+ */
+public final class SaslMechanismsTypeDecoder extends AbstractDescribedTypeDecoder<SaslMechanisms> {
+
+    private static final int REQUIRED_SASL_MECHANISMS_LIST_ENTRIES = 1;
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslMechanisms.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslMechanisms.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<SaslMechanisms> getTypeClass() {
+        return SaslMechanisms.class;
+    }
+
+    @Override
+    public SaslMechanisms readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslMechanisms[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final SaslMechanisms[] result = new SaslMechanisms[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private SaslMechanisms readProperties(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslMechanisms mechanisms = new SaslMechanisms();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count != REQUIRED_SASL_MECHANISMS_LIST_ENTRIES) {
+            throw new DecodeException("SASL Mechanisms must contain at least one mechanisms entry: " + count);
+        } else {
+            mechanisms.setSaslServerMechanisms(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+        }
+
+        return mechanisms;
+    }
+
+    @Override
+    public SaslMechanisms readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslMechanisms[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final SaslMechanisms[] result = new SaslMechanisms[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private SaslMechanisms readProperties(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslMechanisms mechanisms = new SaslMechanisms();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count != REQUIRED_SASL_MECHANISMS_LIST_ENTRIES) {
+            throw new DecodeException("SASL Mechanisms must contain at least one mechanisms entry: " + count);
+        } else {
+            mechanisms.setSaslServerMechanisms(state.getDecoder().readMultiple(stream, state, Symbol.class));
+        }
+
+        return mechanisms;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslOutcomeTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslOutcomeTypeDecoder.java
new file mode 100644
index 0000000..3347ed3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslOutcomeTypeDecoder.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.security;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslCode;
+import org.apache.qpid.protonj2.types.security.SaslOutcome;
+
+/**
+ * Decoder of AMQP SaslOutcome type values from a byte stream.
+ */
+public final class SaslOutcomeTypeDecoder extends AbstractDescribedTypeDecoder<SaslOutcome> {
+
+    private static final int MIN_SASL_OUTCOME_LIST_ENTRIES = 1;
+    private static final int MAX_SASL_OUTCOME_LIST_ENTRIES = 2;
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslOutcome.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslOutcome.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<SaslOutcome> getTypeClass() {
+        return SaslOutcome.class;
+    }
+
+    @Override
+    public SaslOutcome readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslOutcome[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final SaslOutcome[] result = new SaslOutcome[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private SaslOutcome readProperties(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslOutcome outcome = new SaslOutcome();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_SASL_OUTCOME_LIST_ENTRIES) {
+            throw new DecodeException("Not enougn entries in SASL Outcome list encoding: " + count);
+        }
+
+        if (count > MAX_SASL_OUTCOME_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in SASL Outcome list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    outcome.setCode(SaslCode.valueOf(state.getDecoder().readUnsignedByte(buffer, state)));
+                    break;
+                case 1:
+                    outcome.setAdditionalData(state.getDecoder().readBinaryAsBuffer(buffer, state));
+                    break;
+            }
+        }
+
+        return outcome;
+    }
+
+    @Override
+    public SaslOutcome readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslOutcome[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final SaslOutcome[] result = new SaslOutcome[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private SaslOutcome readProperties(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslOutcome outcome = new SaslOutcome();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_SASL_OUTCOME_LIST_ENTRIES) {
+            throw new DecodeException("Not enougn entries in SASL Outcome list encoding: " + count);
+        }
+
+        if (count > MAX_SASL_OUTCOME_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in SASL Outcome list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    outcome.setCode(SaslCode.valueOf(state.getDecoder().readUnsignedByte(stream, state)));
+                    break;
+                case 1:
+                    outcome.setAdditionalData(state.getDecoder().readBinaryAsBuffer(stream, state));
+                    break;
+            }
+        }
+
+        return outcome;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslResponseTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslResponseTypeDecoder.java
new file mode 100644
index 0000000..28e5b22
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/security/SaslResponseTypeDecoder.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.security;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslResponse;
+
+/**
+ * Decoder of AMQP SaslResponse type values from a byte stream.
+ */
+public final class SaslResponseTypeDecoder extends AbstractDescribedTypeDecoder<SaslResponse> {
+
+    private static final int REQUIRED_LIST_ENTRIES = 1;
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslResponse.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslResponse.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<SaslResponse> getTypeClass() {
+        return SaslResponse.class;
+    }
+
+    @Override
+    public SaslResponse readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslResponse[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final SaslResponse[] result = new SaslResponse[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private SaslResponse readProperties(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslResponse response = new SaslResponse();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count != REQUIRED_LIST_ENTRIES) {
+            throw new DecodeException("SASL Response must contain a single response binary: " + count);
+        } else {
+            response.setResponse(state.getDecoder().readBinaryAsBuffer(buffer, state));
+        }
+
+        return response;
+    }
+
+    @Override
+    public SaslResponse readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public SaslResponse[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final SaslResponse[] result = new SaslResponse[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readProperties(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private SaslResponse readProperties(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final SaslResponse response = new SaslResponse();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count != REQUIRED_LIST_ENTRIES) {
+            throw new DecodeException("SASL Response must contain a single response binary: " + count);
+        } else {
+            response.setResponse(state.getDecoder().readBinaryAsBuffer(stream, state));
+        }
+
+        return response;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/CoordinatorTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/CoordinatorTypeDecoder.java
new file mode 100644
index 0000000..344c6f4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/CoordinatorTypeDecoder.java
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transactions;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+
+/**
+ * Decoder of AMQP Coordinator type values from a byte stream.
+ */
+public final class CoordinatorTypeDecoder extends AbstractDescribedTypeDecoder<Coordinator> {
+
+    private static final int MIN_COORDINATOR_LIST_ENTRIES = 0;
+    private static final int MAX_COORDINATOR_LIST_ENTRIES = 1;
+
+    @Override
+    public Class<Coordinator> getTypeClass() {
+        return Coordinator.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Coordinator.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Coordinator.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Coordinator readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readCoordinator(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Coordinator[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Coordinator[] result = new Coordinator[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readCoordinator(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Coordinator readCoordinator(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Coordinator coordinator = new Coordinator();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_COORDINATOR_LIST_ENTRIES) {
+            throw new DecodeException("Not enougn entries in Coordinator list encoding: " + count);
+        } else if (count > MAX_COORDINATOR_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Coordinator list encoding: " + count);
+        } else if (count == 1) {
+            coordinator.setCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+        }
+
+        return coordinator;
+    }
+
+    @Override
+    public Coordinator readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readCoordinator(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Coordinator[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Coordinator[] result = new Coordinator[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readCoordinator(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Coordinator readCoordinator(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Coordinator coordinator = new Coordinator();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_COORDINATOR_LIST_ENTRIES) {
+            throw new DecodeException("Not enougn entries in Coordinator list encoding: " + count);
+        } else if (count > MAX_COORDINATOR_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Coordinator list encoding: " + count);
+        } else if (count == 1) {
+            coordinator.setCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+        }
+
+        return coordinator;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DeclareTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DeclareTypeDecoder.java
new file mode 100644
index 0000000..1172575
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DeclareTypeDecoder.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transactions;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transactions.GlobalTxId;
+
+/**
+ * Decoder of AMQP Declare type values from a byte stream
+ */
+public final class DeclareTypeDecoder extends AbstractDescribedTypeDecoder<Declare> {
+
+    private static final int MIN_DECLARE_LIST_ENTRIES = 0;
+    private static final int MAX_DECLARE_LIST_ENTRIES = 1;
+
+    @Override
+    public Class<Declare> getTypeClass() {
+        return Declare.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Declare.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Declare.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Declare readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readDeclare(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Declare[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Declare[] result = new Declare[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDeclare(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Declare readDeclare(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Declare declare = new Declare();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_DECLARE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Declare list encoding: " + count);
+        } else if (count > MAX_DECLARE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Declare list encoding: " + count);
+        } else if (count == 1) {
+            declare.setGlobalId(state.getDecoder().readObject(buffer, state, GlobalTxId.class));
+        }
+
+        return declare;
+    }
+
+    @Override
+    public Declare readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readDeclare(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Declare[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Declare[] result = new Declare[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDeclare(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Declare readDeclare(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Declare declare = new Declare();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_DECLARE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Declare list encoding: " + count);
+        } else if (count > MAX_DECLARE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Declare list encoding: " + count);
+        } else if (count == 1) {
+            declare.setGlobalId(state.getDecoder().readObject(stream, state, GlobalTxId.class));
+        }
+
+        return declare;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DeclaredTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DeclaredTypeDecoder.java
new file mode 100644
index 0000000..6f9c806
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DeclaredTypeDecoder.java
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transactions;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+
+/**
+ * Decoder of AMQP Declared types from a byte stream.
+ */
+public final class DeclaredTypeDecoder extends AbstractDescribedTypeDecoder<Declared> {
+
+    private static final int MIN_DECLARED_LIST_ENTRIES = 1;
+    private static final int MAX_DECLARED_LIST_ENTRIES = 1;
+
+    @Override
+    public Class<Declared> getTypeClass() {
+        return Declared.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Declared.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Declared.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Declared readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readDeclared(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Declared[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Declared[] result = new Declared[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDeclared(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Declared readDeclared(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Declared declared = new Declared();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_DECLARED_LIST_ENTRIES) {
+            throw new DecodeException("The txn-id field cannot be omitted");
+        } else if (count > MAX_DECLARED_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Declared list encoding: " + count);
+        } else if (count == 1) {
+            declared.setTxnId(state.getDecoder().readBinary(buffer, state));
+        }
+
+        return declared;
+    }
+
+    @Override
+    public Declared readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readDeclared(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Declared[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Declared[] result = new Declared[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDeclared(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Declared readDeclared(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Declared declared = new Declared();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_DECLARED_LIST_ENTRIES) {
+            throw new DecodeException("The txn-id field cannot be omitted");
+        } else if (count > MAX_DECLARED_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Declared list encoding: " + count);
+        } else if (count == 1) {
+            declared.setTxnId(state.getDecoder().readBinary(stream, state));
+        }
+
+        return declared;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DischargeTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DischargeTypeDecoder.java
new file mode 100644
index 0000000..1d7a0c0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/DischargeTypeDecoder.java
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transactions;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+
+/**
+ * Decoder of AMQP Discharge type values from a byte stream.
+ */
+public final class DischargeTypeDecoder extends AbstractDescribedTypeDecoder<Discharge> {
+
+    private static final int MIN_DISCHARGE_LIST_ENTRIES = 1;
+    private static final int MAX_DISCHARGE_LIST_ENTRIES = 2;
+
+    @Override
+    public Class<Discharge> getTypeClass() {
+        return Discharge.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Discharge.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Discharge.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Discharge readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readDischarge(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Discharge[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Discharge[] result = new Discharge[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDischarge(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Discharge readDischarge(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Discharge discharge = new Discharge();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_DISCHARGE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Discharge list encoding: " + count);
+        }
+
+        if (count > MAX_DISCHARGE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Discharge list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    discharge.setTxnId(state.getDecoder().readBinary(buffer, state));
+                    break;
+                case 1:
+                    discharge.setFail(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+            }
+        }
+
+        return discharge;
+    }
+
+    @Override
+    public Discharge readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readDischarge(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Discharge[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Discharge[] result = new Discharge[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDischarge(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Discharge readDischarge(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Discharge discharge = new Discharge();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_DISCHARGE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Discharge list encoding: " + count);
+        }
+
+        if (count > MAX_DISCHARGE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Discharge list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    discharge.setTxnId(state.getDecoder().readBinary(stream, state));
+                    break;
+                case 1:
+                    discharge.setFail(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+            }
+        }
+
+        return discharge;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/TransactionStateTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/TransactionStateTypeDecoder.java
new file mode 100644
index 0000000..131ec4e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transactions/TransactionStateTypeDecoder.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transactions;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+
+/**
+ * Decoder of AMQP TransactionState types from a byte stream.
+ */
+public final class TransactionStateTypeDecoder extends AbstractDescribedTypeDecoder<TransactionalState> {
+
+    private static final int MIN_TRANSACTION_STATE_LIST_ENTRIES = 1;
+    private static final int MAX_TRANSACTION_STATE_LIST_ENTRIES = 2;
+
+    @Override
+    public Class<TransactionalState> getTypeClass() {
+        return TransactionalState.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return TransactionalState.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return TransactionalState.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public TransactionalState readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readTransactionalState(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public TransactionalState[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final TransactionalState[] result = new TransactionalState[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readTransactionalState(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private TransactionalState readTransactionalState(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final TransactionalState transactionalState = new TransactionalState();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_TRANSACTION_STATE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in TransactionalState list encoding: " + count);
+        }
+
+        if (count > MAX_TRANSACTION_STATE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in TransactionalState list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    transactionalState.setTxnId(state.getDecoder().readBinary(buffer, state));
+                    break;
+                case 1:
+                    transactionalState.setOutcome((Outcome) state.getDecoder().readObject(buffer, state));
+                    break;
+            }
+        }
+
+        return transactionalState;
+    }
+
+    @Override
+    public TransactionalState readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readTransactionalState(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public TransactionalState[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final TransactionalState[] result = new TransactionalState[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readTransactionalState(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private TransactionalState readTransactionalState(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final TransactionalState transactionalState = new TransactionalState();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_TRANSACTION_STATE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in TransactionalState list encoding: " + count);
+        }
+
+        if (count > MAX_TRANSACTION_STATE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in TransactionalState list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    transactionalState.setTxnId(state.getDecoder().readBinary(stream, state));
+                    break;
+                case 1:
+                    transactionalState.setOutcome((Outcome) state.getDecoder().readObject(stream, state));
+                    break;
+            }
+        }
+
+        return transactionalState;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/AttachTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/AttachTypeDecoder.java
new file mode 100644
index 0000000..4fb5d06
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/AttachTypeDecoder.java
@@ -0,0 +1,297 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+
+/**
+ * Decoder of AMQP Attach type values from a byte stream.
+ */
+public final class AttachTypeDecoder extends AbstractDescribedTypeDecoder<Attach> {
+
+    private static final int MIN_ATTACH_LIST_ENTRIES = 3;
+    private static final int MAX_ATTACH_LIST_ENTRIES = 14;
+
+    @Override
+    public Class<Attach> getTypeClass() {
+        return Attach.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Attach.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Attach.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Attach readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readAttach(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Attach[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Attach[] result = new Attach[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readAttach(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Attach readAttach(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Attach attach = new Attach();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_ATTACH_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_ATTACH_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Attach list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                // Ensure mandatory fields are set
+                if (index < MIN_ATTACH_LIST_ENTRIES) {
+                    throw new DecodeException(errorForMissingRequiredFields(index));
+                }
+
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    attach.setName(state.getDecoder().readString(buffer, state));
+                    break;
+                case 1:
+                    attach.setHandle(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 2:
+                    Boolean role = state.getDecoder().readBoolean(buffer, state);
+                    attach.setRole(Boolean.TRUE.equals(role) ? Role.RECEIVER : Role.SENDER);
+                    break;
+                case 3:
+                    byte sndSettleMode = state.getDecoder().readUnsignedByte(buffer, state, (byte) 2);
+                    attach.setSenderSettleMode(SenderSettleMode.valueOf(sndSettleMode));
+                    break;
+                case 4:
+                    byte rcvSettleMode = state.getDecoder().readUnsignedByte(buffer, state, (byte) 0);
+                    attach.setReceiverSettleMode(ReceiverSettleMode.valueOf(rcvSettleMode));
+                    break;
+                case 5:
+                    attach.setSource(state.getDecoder().readObject(buffer, state, Source.class));
+                    break;
+                case 6:
+                    attach.setTarget(state.getDecoder().readObject(buffer, state, Terminus.class));
+                    break;
+                case 7:
+                    attach.setUnsettled(state.getDecoder().readMap(buffer, state));
+                    break;
+                case 8:
+                    attach.setIncompleteUnsettled(state.getDecoder().readBoolean(buffer, state, true));
+                    break;
+                case 9:
+                    attach.setInitialDeliveryCount(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 10:
+                    attach.setMaxMessageSize(state.getDecoder().readUnsignedLong(buffer, state));
+                    break;
+                case 11:
+                    attach.setOfferedCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 12:
+                    attach.setDesiredCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 13:
+                    attach.setProperties(state.getDecoder().readMap(buffer, state));
+                    break;
+            }
+        }
+
+        return attach;
+    }
+
+    @Override
+    public Attach readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readAttach(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Attach[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Attach[] result = new Attach[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readAttach(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Attach readAttach(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Attach attach = new Attach();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_ATTACH_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_ATTACH_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Attach list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    // Ensure mandatory fields are set
+                    if (index < MIN_ATTACH_LIST_ENTRIES) {
+                        throw new DecodeException(errorForMissingRequiredFields(index));
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    attach.setName(state.getDecoder().readString(stream, state));
+                    break;
+                case 1:
+                    attach.setHandle(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 2:
+                    Boolean role = state.getDecoder().readBoolean(stream, state);
+                    attach.setRole(Boolean.TRUE.equals(role) ? Role.RECEIVER : Role.SENDER);
+                    break;
+                case 3:
+                    byte sndSettleMode = state.getDecoder().readUnsignedByte(stream, state, (byte) 2);
+                    attach.setSenderSettleMode(SenderSettleMode.valueOf(sndSettleMode));
+                    break;
+                case 4:
+                    byte rcvSettleMode = state.getDecoder().readUnsignedByte(stream, state, (byte) 0);
+                    attach.setReceiverSettleMode(ReceiverSettleMode.valueOf(rcvSettleMode));
+                    break;
+                case 5:
+                    attach.setSource(state.getDecoder().readObject(stream, state, Source.class));
+                    break;
+                case 6:
+                    attach.setTarget(state.getDecoder().readObject(stream, state, Terminus.class));
+                    break;
+                case 7:
+                    attach.setUnsettled(state.getDecoder().readMap(stream, state));
+                    break;
+                case 8:
+                    attach.setIncompleteUnsettled(state.getDecoder().readBoolean(stream, state, true));
+                    break;
+                case 9:
+                    attach.setInitialDeliveryCount(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 10:
+                    attach.setMaxMessageSize(state.getDecoder().readUnsignedLong(stream, state));
+                    break;
+                case 11:
+                    attach.setOfferedCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 12:
+                    attach.setDesiredCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 13:
+                    attach.setProperties(state.getDecoder().readMap(stream, state));
+                    break;
+            }
+        }
+
+        return attach;
+    }
+
+    private String errorForMissingRequiredFields(int present) {
+        switch (present) {
+            case 2:
+                return "The role field cannot be omitted";
+            case 1:
+                return "The handle field cannot be omitted";
+            default:
+                return "The name field cannot be omitted";
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/BeginTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/BeginTypeDecoder.java
new file mode 100644
index 0000000..5c887f9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/BeginTypeDecoder.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Begin;
+
+/**
+ * Decoder of AMQP Begin type values from a byte stream
+ */
+public final class BeginTypeDecoder extends AbstractDescribedTypeDecoder<Begin> {
+
+    private static final int MIN_BEGIN_LIST_ENTRIES = 4;
+    private static final int MAX_BEGIN_LIST_ENTRIES = 8;
+
+    @Override
+    public Class<Begin> getTypeClass() {
+        return Begin.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Begin.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Begin.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Begin readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readBegin(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Begin[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Begin[] result = new Begin[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readBegin(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Begin readBegin(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Begin begin = new Begin();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_BEGIN_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_BEGIN_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Begin list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                // Ensure mandatory fields are set
+                if (index > 0 && index < MIN_BEGIN_LIST_ENTRIES) {
+                    throw new DecodeException(errorForMissingRequiredFields(index));
+                }
+
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    begin.setRemoteChannel(state.getDecoder().readUnsignedShort(buffer, state, 0));
+                    break;
+                case 1:
+                    begin.setNextOutgoingId(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 2:
+                    begin.setIncomingWindow(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 3:
+                    begin.setOutgoingWindow(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 4:
+                    begin.setHandleMax(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 5:
+                    begin.setOfferedCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 6:
+                    begin.setDesiredCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 7:
+                    begin.setProperties(state.getDecoder().readMap(buffer, state));
+                    break;
+            }
+        }
+
+        return begin;
+    }
+
+    @Override
+    public Begin readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readBegin(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Begin[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Begin[] result = new Begin[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readBegin(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Begin readBegin(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Begin begin = new Begin();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_BEGIN_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_BEGIN_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Begin list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    // Ensure mandatory fields are set
+                    if (index > 0 && index < MIN_BEGIN_LIST_ENTRIES) {
+                        throw new DecodeException(errorForMissingRequiredFields(index));
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    begin.setRemoteChannel(state.getDecoder().readUnsignedShort(stream, state, 0));
+                    break;
+                case 1:
+                    begin.setNextOutgoingId(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 2:
+                    begin.setIncomingWindow(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 3:
+                    begin.setOutgoingWindow(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 4:
+                    begin.setHandleMax(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 5:
+                    begin.setOfferedCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 6:
+                    begin.setDesiredCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 7:
+                    begin.setProperties(state.getDecoder().readMap(stream, state));
+                    break;
+            }
+        }
+
+        return begin;
+    }
+
+    private String errorForMissingRequiredFields(int present) {
+        switch (present) {
+            case 3:
+                return "The outgoing-window field cannot be omitted";
+            case 2:
+                return "The incoming-window field cannot be omitted";
+            default:
+                return "The next-outgoing-id field cannot be omitted";
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/CloseTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/CloseTypeDecoder.java
new file mode 100644
index 0000000..44f6109
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/CloseTypeDecoder.java
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Decoder of AMQP Close type values from a byte stream
+ */
+public final class CloseTypeDecoder extends AbstractDescribedTypeDecoder<Close> {
+
+    private static final int MIN_CLOSE_LIST_ENTRIES = 0;
+    private static final int MAX_CLOSE_LIST_ENTRIES = 1;
+
+    @Override
+    public Class<Close> getTypeClass() {
+        return Close.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Close.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Close.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Close readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readClose(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Close[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Close[] result = new Close[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readClose(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Close readClose(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Close close = new Close();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_CLOSE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Close list encoding: " + count);
+        } else if (count > MAX_CLOSE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Close list encoding: " + count);
+        } else if (count == 1) {
+            close.setError(state.getDecoder().readObject(buffer, state, ErrorCondition.class));
+        }
+
+        return close;
+    }
+
+    @Override
+    public Close readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readClose(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Close[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Close[] result = new Close[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readClose(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Close readClose(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Close close = new Close();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_CLOSE_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in Close list encoding: " + count);
+        } if (count > MAX_CLOSE_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Close list encoding: " + count);
+        } else if (count == 1) {
+            close.setError(state.getDecoder().readObject(stream, state, ErrorCondition.class));
+        }
+
+        return close;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/DetachTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/DetachTypeDecoder.java
new file mode 100644
index 0000000..46c9e2a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/DetachTypeDecoder.java
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Decoder of AMQP Detach type values from a byte stream
+ */
+public final class DetachTypeDecoder extends AbstractDescribedTypeDecoder<Detach> {
+
+    private static final int MIN_DETACH_LIST_ENTRIES = 1;
+    private static final int MAX_DETACH_LIST_ENTRIES = 3;
+
+    @Override
+    public Class<Detach> getTypeClass() {
+        return Detach.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Detach.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Detach.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Detach readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readDetach(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Detach[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Detach[] result = new Detach[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDetach(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Detach readDetach(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Detach detach = new Detach();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_DETACH_LIST_ENTRIES) {
+            throw new DecodeException("The handle field is mandatory");
+        }
+
+        if (count > MAX_DETACH_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Detach list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                if (index == 0) {
+                    throw new DecodeException("The handle field is mandatory");
+                }
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    detach.setHandle(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 1:
+                    detach.setClosed(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 2:
+                    detach.setError(state.getDecoder().readObject(buffer, state, ErrorCondition.class));
+                    break;
+            }
+        }
+
+        return detach;
+    }
+
+    @Override
+    public Detach readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readDetach(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Detach[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Detach[] result = new Detach[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDetach(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Detach readDetach(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Detach detach = new Detach();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_DETACH_LIST_ENTRIES) {
+            throw new DecodeException("The handle field is mandatory");
+        }
+
+        if (count > MAX_DETACH_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Detach list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    if (index == 0) {
+                        throw new DecodeException("The handle field is mandatory");
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    detach.setHandle(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 1:
+                    detach.setClosed(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 2:
+                    detach.setError(state.getDecoder().readObject(stream, state, ErrorCondition.class));
+                    break;
+            }
+        }
+
+        return detach;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/DispositionTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/DispositionTypeDecoder.java
new file mode 100644
index 0000000..6d6b612
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/DispositionTypeDecoder.java
@@ -0,0 +1,238 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Role;
+
+/**
+ * Decoder of AMQP Disposition type values from a byte stream.
+ */
+public final class DispositionTypeDecoder extends AbstractDescribedTypeDecoder<Disposition> {
+
+    private static final int MIN_DISPOSITION_LIST_ENTRIES = 2;
+    private static final int MAX_DISPOSITION_LIST_ENTRIES = 6;
+
+    @Override
+    public Class<Disposition> getTypeClass() {
+        return Disposition.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Disposition.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Disposition.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Disposition readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readDisposition(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Disposition[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        Disposition[] result = new Disposition[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDisposition(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Disposition readDisposition(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Disposition disposition = new Disposition();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_DISPOSITION_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_DISPOSITION_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Disposition list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                // Ensure mandatory fields are set
+                if (index < MIN_DISPOSITION_LIST_ENTRIES) {
+                    throw new DecodeException(errorForMissingRequiredFields(index));
+                }
+
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    disposition.setRole(Boolean.TRUE.equals(state.getDecoder().readBoolean(buffer, state)) ? Role.RECEIVER : Role.SENDER);
+                    break;
+                case 1:
+                    disposition.setFirst(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 2:
+                    disposition.setLast(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 3:
+                    disposition.setSettled(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 4:
+                    disposition.setState(state.getDecoder().readObject(buffer, state, DeliveryState.class));
+                    break;
+                case 5:
+                    disposition.setBatchable(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+            }
+        }
+
+        return disposition;
+    }
+
+    private String errorForMissingRequiredFields(int present) {
+        switch (present) {
+            case 1:
+                return "The first field cannot be omitted";
+            default:
+                return "The role field cannot be omitted";
+        }
+    }
+
+    @Override
+    public Disposition readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readDisposition(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Disposition[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Disposition[] result = new Disposition[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readDisposition(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Disposition readDisposition(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Disposition disposition = new Disposition();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_DISPOSITION_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_DISPOSITION_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Disposition list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    // Ensure mandatory fields are set
+                    if (index < MIN_DISPOSITION_LIST_ENTRIES) {
+                        throw new DecodeException(errorForMissingRequiredFields(index));
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    disposition.setRole(Boolean.TRUE.equals(state.getDecoder().readBoolean(stream, state)) ? Role.RECEIVER : Role.SENDER);
+                    break;
+                case 1:
+                    disposition.setFirst(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 2:
+                    disposition.setLast(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 3:
+                    disposition.setSettled(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 4:
+                    disposition.setState(state.getDecoder().readObject(stream, state, DeliveryState.class));
+                    break;
+                case 5:
+                    disposition.setBatchable(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+            }
+        }
+
+        return disposition;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/EndTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/EndTypeDecoder.java
new file mode 100644
index 0000000..11f5e78
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/EndTypeDecoder.java
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Decoder of AMQP End type values from a byte stream
+ */
+public final class EndTypeDecoder extends AbstractDescribedTypeDecoder<End> {
+
+    private static final int MIN_END_LIST_ENTRIES = 0;
+    private static final int MAX_END_LIST_ENTRIES = 1;
+
+    @Override
+    public Class<End> getTypeClass() {
+        return End.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return End.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return End.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public End readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readEnd(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public End[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final End[] result = new End[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readEnd(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private End readEnd(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final End end = new End();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_END_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in End list encoding: " + count);
+        }
+
+        if (count > MAX_END_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in End list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    end.setError(state.getDecoder().readObject(buffer, state, ErrorCondition.class));
+                    break;
+            }
+        }
+
+        return end;
+    }
+
+    @Override
+    public End readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readEnd(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public End[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final End[] result = new End[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readEnd(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private End readEnd(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final End end = new End();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_END_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in End list encoding: " + count);
+        }
+
+        if (count > MAX_END_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in End list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    end.setError(state.getDecoder().readObject(stream, state, ErrorCondition.class));
+                    break;
+            }
+        }
+
+        return end;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/ErrorConditionTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/ErrorConditionTypeDecoder.java
new file mode 100644
index 0000000..a8d09fd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/ErrorConditionTypeDecoder.java
@@ -0,0 +1,180 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Decoder of AMQP ErrorCondition type values from a byte stream.
+ */
+public final class ErrorConditionTypeDecoder extends AbstractDescribedTypeDecoder<ErrorCondition> {
+
+    private static final int MIN_ERROR_CONDITION_LIST_ENTRIES = 1;
+    private static final int MAX_ERROR_CONDITION_LIST_ENTRIES = 3;
+
+    @Override
+    public Class<ErrorCondition> getTypeClass() {
+        return ErrorCondition.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return ErrorCondition.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return ErrorCondition.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public ErrorCondition readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readErrorCondition(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public ErrorCondition[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final ErrorCondition[] result = new ErrorCondition[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readErrorCondition(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private ErrorCondition readErrorCondition(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_ERROR_CONDITION_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in ErrorCondition list encoding: " + count);
+        }
+        if (count > MAX_ERROR_CONDITION_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in ErrorCondition list encoding: " + count);
+        }
+
+        Symbol condition = null;
+        String description = null;
+        Map<Symbol, Object> info = null;
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    condition = state.getDecoder().readSymbol(buffer, state);
+                    break;
+                case 1:
+                    description = state.getDecoder().readString(buffer, state);
+                    break;
+                case 2:
+                    info = state.getDecoder().readMap(buffer, state);
+                    break;
+            }
+        }
+
+        return new ErrorCondition(condition, description, info);
+    }
+
+    @Override
+    public ErrorCondition readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readErrorCondition(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public ErrorCondition[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final ErrorCondition[] result = new ErrorCondition[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readErrorCondition(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private ErrorCondition readErrorCondition(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_ERROR_CONDITION_LIST_ENTRIES) {
+            throw new DecodeException("Not enough entries in ErrorCondition list encoding: " + count);
+        }
+        if (count > MAX_ERROR_CONDITION_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in ErrorCondition list encoding: " + count);
+        }
+
+        Symbol condition = null;
+        String description = null;
+        Map<Symbol, Object> info = null;
+
+        for (int index = 0; index < count; ++index) {
+            switch (index) {
+                case 0:
+                    condition = state.getDecoder().readSymbol(stream, state);
+                    break;
+                case 1:
+                    description = state.getDecoder().readString(stream, state);
+                    break;
+                case 2:
+                    info = state.getDecoder().readMap(stream, state);
+                    break;
+            }
+        }
+
+        return new ErrorCondition(condition, description, info);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/FlowTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/FlowTypeDecoder.java
new file mode 100644
index 0000000..3132a9f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/FlowTypeDecoder.java
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Flow;
+
+/**
+ * Decoder of AMQP Flow type values from a byte stream.
+ */
+public final class FlowTypeDecoder extends AbstractDescribedTypeDecoder<Flow> {
+
+    private static final int MIN_FLOW_LIST_ENTRIES = 4;
+    private static final int MAX_FLOW_LIST_ENTRIES = 11;
+
+    @Override
+    public Class<Flow> getTypeClass() {
+        return Flow.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Flow.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Flow.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Flow readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readFlow(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Flow[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Flow[] result = new Flow[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readFlow(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Flow readFlow(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Flow flow = new Flow();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_FLOW_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_FLOW_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Flow list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                // Ensure mandatory fields are set
+                if (index > 0 && index < MIN_FLOW_LIST_ENTRIES) {
+                    throw new DecodeException(errorForMissingRequiredFields(index));
+                }
+
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    flow.setNextIncomingId(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 1:
+                    flow.setIncomingWindow(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 2:
+                    flow.setNextOutgoingId(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 3:
+                    flow.setOutgoingWindow(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 4:
+                    flow.setHandle(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 5:
+                    flow.setDeliveryCount(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 6:
+                    flow.setLinkCredit(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 7:
+                    flow.setAvailable(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 8:
+                    flow.setDrain(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 9:
+                    flow.setEcho(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 10:
+                    flow.setProperties(state.getDecoder().readMap(buffer, state));
+                    break;
+            }
+        }
+
+        return flow;
+    }
+
+    @Override
+    public Flow readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readFlow(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Flow[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Flow[] result = new Flow[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readFlow(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Flow readFlow(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Flow flow = new Flow();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_FLOW_LIST_ENTRIES) {
+            throw new DecodeException(errorForMissingRequiredFields(count));
+        }
+
+        if (count > MAX_FLOW_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Flow list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    // Ensure mandatory fields are set
+                    if (index > 0 && index < MIN_FLOW_LIST_ENTRIES) {
+                        throw new DecodeException(errorForMissingRequiredFields(index));
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    flow.setNextIncomingId(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 1:
+                    flow.setIncomingWindow(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 2:
+                    flow.setNextOutgoingId(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 3:
+                    flow.setOutgoingWindow(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 4:
+                    flow.setHandle(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 5:
+                    flow.setDeliveryCount(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 6:
+                    flow.setLinkCredit(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 7:
+                    flow.setAvailable(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 8:
+                    flow.setDrain(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 9:
+                    flow.setEcho(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 10:
+                    flow.setProperties(state.getDecoder().readMap(stream, state));
+                    break;
+            }
+        }
+
+        return flow;
+    }
+
+    private String errorForMissingRequiredFields(int present) {
+        switch (present) {
+            case 3:
+                return "The outgoing-window field cannot be omitted";
+            case 2:
+                return "The next-outgoing-id field cannot be omitted";
+            default:
+                return "The incoming-window field cannot be omitted";
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/OpenTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/OpenTypeDecoder.java
new file mode 100644
index 0000000..040345f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/OpenTypeDecoder.java
@@ -0,0 +1,248 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Open;
+
+/**
+ * Decoder of AMQP Open type values from a byte stream.
+ */
+public final class OpenTypeDecoder extends AbstractDescribedTypeDecoder<Open> {
+
+    private static final int MIN_OPEN_LIST_ENTRIES = 1;
+    private static final int MAX_OPEN_LIST_ENTRIES = 10;
+
+    @Override
+    public Class<Open> getTypeClass() {
+        return Open.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Open.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Open.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Open readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readOpen(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Open[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Open[] result = new Open[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readOpen(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Open readOpen(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Open open = new Open();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        if (count < MIN_OPEN_LIST_ENTRIES) {
+            throw new DecodeException("The container-id field cannot be omitted");
+        }
+
+        if (count > MAX_OPEN_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Open list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                if (index == 0) {
+                    throw new DecodeException("The container-id field cannot be omitted");
+                }
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    open.setContainerId(state.getDecoder().readString(buffer, state));
+                    break;
+                case 1:
+                    open.setHostname(state.getDecoder().readString(buffer, state));
+                    break;
+                case 2:
+                    open.setMaxFrameSize(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 3:
+                    open.setChannelMax(state.getDecoder().readUnsignedShort(buffer, state, 0));
+                    break;
+                case 4:
+                    open.setIdleTimeout(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 5:
+                    open.setOutgoingLocales(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 6:
+                    open.setIncomingLocales(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 7:
+                    open.setOfferedCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 8:
+                    open.setDesiredCapabilities(state.getDecoder().readMultiple(buffer, state, Symbol.class));
+                    break;
+                case 9:
+                    open.setProperties(state.getDecoder().readMap(buffer, state));
+                    break;
+            }
+        }
+
+        return open;
+    }
+
+    @Override
+    public Open readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readOpen(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Open[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Open[] result = new Open[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readOpen(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Open readOpen(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Open open = new Open();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        if (count < MIN_OPEN_LIST_ENTRIES) {
+            throw new DecodeException("The container-id field cannot be omitted");
+        }
+
+        if (count > MAX_OPEN_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Open list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    if (index == 0) {
+                        throw new DecodeException("The container-id field cannot be omitted");
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    open.setContainerId(state.getDecoder().readString(stream, state));
+                    break;
+                case 1:
+                    open.setHostname(state.getDecoder().readString(stream, state));
+                    break;
+                case 2:
+                    open.setMaxFrameSize(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 3:
+                    open.setChannelMax(state.getDecoder().readUnsignedShort(stream, state, 0));
+                    break;
+                case 4:
+                    open.setIdleTimeout(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 5:
+                    open.setOutgoingLocales(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 6:
+                    open.setIncomingLocales(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 7:
+                    open.setOfferedCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 8:
+                    open.setDesiredCapabilities(state.getDecoder().readMultiple(stream, state, Symbol.class));
+                    break;
+                case 9:
+                    open.setProperties(state.getDecoder().readMap(stream, state));
+                    break;
+            }
+        }
+
+        return open;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/TransferTypeDecoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/TransferTypeDecoder.java
new file mode 100644
index 0000000..ee978cb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/decoders/transport/TransferTypeDecoder.java
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders.transport;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamUtils;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ListTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Decoder of AMQP Transfer type values from a byte stream
+ */
+public final class TransferTypeDecoder extends AbstractDescribedTypeDecoder<Transfer> {
+
+    private static final int MIN_TRANSFER_LIST_ENTRIES = 1;
+    private static final int MAX_TRANSFER_LIST_ENTRIES = 11;
+
+    @Override
+    public Class<Transfer> getTypeClass() {
+        return Transfer.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Transfer.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Transfer.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Transfer readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        return readTransfer(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Transfer[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        final Transfer[] result = new Transfer[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readTransfer(buffer, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        final TypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(buffer, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(buffer, state);
+    }
+
+    private Transfer readTransfer(ProtonBuffer buffer, DecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Transfer transfer = new Transfer();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(buffer);
+        final int count = listDecoder.readCount(buffer);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_TRANSFER_LIST_ENTRIES) {
+            throw new DecodeException("The handle field cannot be omitted");
+        }
+
+        if (count > MAX_TRANSFER_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Transfer list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // Peek ahead and see if there is a null in the next slot, if so we don't call
+            // the setter for that entry to ensure the returned type reflects the encoded
+            // state in the modification entry.
+            final boolean nullValue = buffer.getByte(buffer.getReadIndex()) == EncodingCodes.NULL;
+            if (nullValue) {
+                if (index == 0) {
+                    throw new DecodeException("The handle field cannot be omitted");
+                }
+
+                buffer.readByte();
+                continue;
+            }
+
+            switch (index) {
+                case 0:
+                    transfer.setHandle(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 1:
+                    transfer.setDeliveryId(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 2:
+                    transfer.setDeliveryTag(state.getDecoder().readDeliveryTag(buffer, state));
+                    break;
+                case 3:
+                    transfer.setMessageFormat(state.getDecoder().readUnsignedInteger(buffer, state, 0l));
+                    break;
+                case 4:
+                    transfer.setSettled(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 5:
+                    transfer.setMore(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 6:
+                    final UnsignedByte rcvSettleMode = state.getDecoder().readUnsignedByte(buffer, state);
+                    transfer.setRcvSettleMode(rcvSettleMode == null ? null : ReceiverSettleMode.values()[rcvSettleMode.intValue()]);
+                    break;
+                case 7:
+                    transfer.setState(state.getDecoder().readObject(buffer, state, DeliveryState.class));
+                    break;
+                case 8:
+                    transfer.setResume(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 9:
+                    transfer.setAborted(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+                case 10:
+                    transfer.setBatchable(state.getDecoder().readBoolean(buffer, state, false));
+                    break;
+            }
+        }
+
+        return transfer;
+    }
+
+    @Override
+    public Transfer readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        return readTransfer(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+    }
+
+    @Override
+    public Transfer[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        final Transfer[] result = new Transfer[count];
+        for (int i = 0; i < count; ++i) {
+            result[i] = readTransfer(stream, state, checkIsExpectedTypeAndCast(ListTypeDecoder.class, decoder));
+        }
+
+        return result;
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        final StreamTypeDecoder<?> decoder = state.getDecoder().readNextTypeDecoder(stream, state);
+
+        checkIsExpectedType(ListTypeDecoder.class, decoder);
+
+        decoder.skipValue(stream, state);
+    }
+
+    private Transfer readTransfer(InputStream stream, StreamDecoderState state, ListTypeDecoder listDecoder) throws DecodeException {
+        final Transfer transfer = new Transfer();
+
+        @SuppressWarnings("unused")
+        final int size = listDecoder.readSize(stream);
+        final int count = listDecoder.readCount(stream);
+
+        // Don't decode anything if things already look wrong.
+        if (count < MIN_TRANSFER_LIST_ENTRIES) {
+            throw new DecodeException("The handle field cannot be omitted");
+        }
+
+        if (count > MAX_TRANSFER_LIST_ENTRIES) {
+            throw new DecodeException("To many entries in Transfer list encoding: " + count);
+        }
+
+        for (int index = 0; index < count; ++index) {
+            // If the stream allows we peek ahead and see if there is a null in the next slot,
+            // if so we don't call the setter for that entry to ensure the returned type reflects
+            // the encoded state in the modification entry.
+            if (stream.markSupported()) {
+                stream.mark(1);
+                final boolean nullValue = ProtonStreamUtils.readByte(stream) == EncodingCodes.NULL;
+                if (nullValue) {
+                    if (index == 0) {
+                        throw new DecodeException("The handle field cannot be omitted");
+                    }
+
+                    continue;
+                } else {
+                    ProtonStreamUtils.reset(stream);
+                }
+            }
+
+            switch (index) {
+                case 0:
+                    transfer.setHandle(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 1:
+                    transfer.setDeliveryId(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 2:
+                    transfer.setDeliveryTag(state.getDecoder().readDeliveryTag(stream, state));
+                    break;
+                case 3:
+                    transfer.setMessageFormat(state.getDecoder().readUnsignedInteger(stream, state, 0l));
+                    break;
+                case 4:
+                    transfer.setSettled(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 5:
+                    transfer.setMore(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 6:
+                    final UnsignedByte rcvSettleMode = state.getDecoder().readUnsignedByte(stream, state);
+                    transfer.setRcvSettleMode(rcvSettleMode == null ? null : ReceiverSettleMode.values()[rcvSettleMode.intValue()]);
+                    break;
+                case 7:
+                    transfer.setState(state.getDecoder().readObject(stream, state, DeliveryState.class));
+                    break;
+                case 8:
+                    transfer.setResume(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 9:
+                    transfer.setAborted(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+                case 10:
+                    transfer.setBatchable(state.getDecoder().readBoolean(stream, state, false));
+                    break;
+            }
+        }
+
+        return transfer;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedListTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedListTypeEncoder.java
new file mode 100644
index 0000000..ffa870e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedListTypeEncoder.java
@@ -0,0 +1,195 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+
+/**
+ * Base class used for all Described Type objects that are represented as a List
+ *
+ * @param <V> the type that is being encoded
+ */
+public abstract class AbstractDescribedListTypeEncoder<V> extends AbstractDescribedTypeEncoder<V> {
+
+    /**
+     * Determine the list type the given value can be encoded to based on the number
+     * of bytes that would be needed to hold the encoded form of the resulting list
+     * entries.
+     * <p>
+     * Most encoders will return LIST32 but for cases where the type is known to
+     * be encoded to LIST8 or always encodes an empty list (LIST0) the encoder can
+     * optimize the encode step and not compute sizes.
+     *
+     * @param value
+     *      The value that is to be encoded.
+     *
+     * @return the encoding code of the list type encoding needed for this object.
+     */
+    public byte getListEncoding(V value) {
+        return EncodingCodes.LIST32;
+    }
+
+    /**
+     * Instructs the encoder to write the element identified with the given index
+     *
+     * @param source
+     *      the source of the list elements to write
+     * @param index
+     *      the element index that needs to be written
+     * @param buffer
+     *      the buffer to write the element to
+     * @param state
+     *      the current EncoderState value to use.
+     */
+    public abstract void writeElement(V source, int index, ProtonBuffer buffer, EncoderState state);
+
+    /**
+     * Gets the number of elements that will result when this type is encoded
+     * into an AMQP List type.
+     *
+     * @param value
+     *      the value which will be encoded as a list type.
+     *
+     * @return the number of elements that should comprise the encoded list.
+     */
+    public abstract int getElementCount(V value);
+
+    /**
+     * Return the minimum number of elements that this AMQP type must provide
+     * in order to be considered a valid type.
+     *
+     * @return the minimum number of elements this type must provide.
+     */
+    public int getMinElementCount() {
+        return 0;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, V value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode().byteValue());
+
+        final int count = getElementCount(value);
+        final byte encodingCode = getListEncoding(value);
+
+        if (count < getMinElementCount()) {
+            throw new EncodeException("Incomplete Type cannot be encoded");
+        }
+
+        buffer.writeByte(encodingCode);
+
+        switch (encodingCode) {
+            case EncodingCodes.LIST8:
+                writeSmallType(buffer, state, value, count);
+                break;
+            case EncodingCodes.LIST32:
+                writeLargeType(buffer, state, value, count);
+                break;
+        }
+    }
+
+    private void writeSmallType(ProtonBuffer buffer, EncoderState state, V value, int elementCount) {
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeByte((byte) 0);
+        buffer.writeByte((byte) elementCount);
+
+        // Write the list elements and then compute total size written.
+        for (int i = 0; i < elementCount; ++i) {
+            writeElement(value, i, buffer, state);
+        }
+
+        // Move back and write the size
+        final int writeSize = buffer.getWriteIndex() - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, writeSize);
+    }
+
+    private void writeLargeType(ProtonBuffer buffer, EncoderState state, V value, int elementCount) {
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(elementCount);
+
+        // Write the list elements and then compute total size written.
+        for (int i = 0; i < elementCount; ++i) {
+            writeElement(value, i, buffer, state);
+        }
+
+        // Move back and write the size
+        final int writeSize = buffer.getWriteIndex() - startIndex - Integer.BYTES;
+
+        buffer.setInt(startIndex, writeSize);
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        final int writeSize = buffer.getWriteIndex() - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, writeSize);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.LIST32);
+
+        for (int i = 0; i < values.length; ++i) {
+            final V listType = (V) values[i];
+            final int count = getElementCount(listType);
+            final int elementStartIndex = buffer.getWriteIndex();
+
+            // Reserve space for the size and write the count of list elements.
+            buffer.writeInt(0);
+            buffer.writeInt(count);
+
+            // Write the list elements and then compute total size written.
+            for (int j = 0; j < count; ++j) {
+                writeElement(listType, j, buffer, state);
+            }
+
+            // Move back and write the size
+            final int listWriteSize = buffer.getWriteIndex() - elementStartIndex - Integer.BYTES;
+
+            buffer.setInt(elementStartIndex, listWriteSize);
+        }
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedMapTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedMapTypeEncoder.java
new file mode 100644
index 0000000..29a4e0e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedMapTypeEncoder.java
@@ -0,0 +1,188 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+
+/**
+ * Base class used for all Described Type objects that are represented as a List
+ *
+ * @param <M> the map type that is being encoded.
+ * @param <K> the key type used for the encoded map's keys.
+ * @param <V> the value type used for the encoded map's values.
+ */
+public abstract class AbstractDescribedMapTypeEncoder<K, V, M> extends AbstractDescribedTypeEncoder<M> {
+
+    /**
+     * Determine the map type the given value can be encoded to based on the number
+     * of bytes that would be needed to hold the encoded form of the resulting list
+     * entries.
+     * <p>
+     * Most encoders will return MAP32 but for cases where the type is known to
+     * be encoded to MAP8 the encoder can optimize the encode step and not compute
+     * sizes.
+     *
+     * @param value
+     *      The value that is to be encoded.
+     *
+     * @return the encoding code of the map type encoding needed for this object.
+     */
+    public byte getMapEncoding(M value) {
+        return EncodingCodes.MAP32;
+    }
+
+    /**
+     * Returns false when the value to be encoded has no Map body and can be
+     * written as a Null body type instead of a Map type.
+     *
+     * @param value
+     *		the value which be encoded as a map type.
+     *
+     * @return true if the type to be encoded has a Map body, false otherwise.
+     */
+    public abstract boolean hasMap(M value);
+
+    /**
+     * Gets the number of elements that will result when this type is encoded
+     * into an AMQP Map type.
+     *
+     * @param value
+     * 		the value which will be encoded as a map type.
+     *
+     * @return the number of elements that should comprise the encoded list.
+     */
+    public abstract int getMapSize(M value);
+
+    /**
+     * Performs the write of the Map entries to the given buffer, the caller
+     * takes care of writing the Map preamble and tracking the final size of
+     * the written elements of the Map.
+     *
+     * @param buffer
+     *      the buffer where the type should be encoded to.
+     * @param state
+     *      the current encoder state.
+     * @param value
+     * 		the value which will be encoded as a map type.
+     */
+    public abstract void writeMapEntries(ProtonBuffer buffer, EncoderState state, M value);
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, M value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode().byteValue());
+
+        if (hasMap(value)) {
+            final int count = getMapSize(value);
+            final byte encodingCode = getMapEncoding(value);
+
+            buffer.writeByte(encodingCode);
+
+            switch (encodingCode) {
+                case EncodingCodes.MAP8:
+                    writeSmallType(buffer, state, value, count);
+                    break;
+                case EncodingCodes.MAP32:
+                    writeLargeType(buffer, state, value, count);
+                    break;
+            }
+        } else {
+            state.getEncoder().writeNull(buffer, state);
+        }
+    }
+
+    private void writeSmallType(ProtonBuffer buffer, EncoderState state, M value, int elementCount) {
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeByte((byte) 0);
+        buffer.writeByte((byte) (elementCount * 2));
+
+        writeMapEntries(buffer, state, value);
+
+        // Move back and write the size
+        final int writeSize = (buffer.getWriteIndex() - startIndex) - Byte.BYTES;
+
+        buffer.setByte(startIndex, writeSize);
+    }
+
+    private void writeLargeType(ProtonBuffer buffer, EncoderState state, M value, int elementCount) {
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(elementCount * 2);
+
+        writeMapEntries(buffer, state, value);
+
+        // Move back and write the size
+        final int writeSize = (buffer.getWriteIndex() - startIndex) - Integer.BYTES;
+
+        buffer.setInt(startIndex, writeSize);
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        final int writeSize = buffer.getWriteIndex() - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, writeSize);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.MAP32);
+
+        for (int i = 0; i < values.length; ++i) {
+            final M map = (M) values[i];
+            final int count = getMapSize(map);
+            final int mapStartIndex = buffer.getWriteIndex();
+
+            // Reserve space for the size and write the count of list elements.
+            buffer.writeInt(0);
+            buffer.writeInt(count * 2);
+
+            writeMapEntries(buffer, state, map);
+
+            // Move back and write the size
+            final int writeSize = buffer.getWriteIndex() - mapStartIndex - Integer.BYTES;
+
+            buffer.setInt(mapStartIndex, writeSize);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedTypeEncoder.java
new file mode 100644
index 0000000..068c210
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractDescribedTypeEncoder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.codec.DescribedTypeEncoder;
+
+/**
+ * Abstract DescribedType encoder implementation
+ *
+ * @param <V> The type that this encoder handles
+ */
+public abstract class AbstractDescribedTypeEncoder<V> implements DescribedTypeEncoder<V> {
+
+    @Override
+    public boolean isArrayType() {
+        return false;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractPrimitiveTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractPrimitiveTypeEncoder.java
new file mode 100644
index 0000000..f1b8cc3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/AbstractPrimitiveTypeEncoder.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+
+/**
+ * Abstract implementation of the PrimitiveTypeEncoder that implements the common methods
+ * that most of the primitive type
+ *
+ * @param <V> The type that this primitive encoder handles
+ */
+public abstract class AbstractPrimitiveTypeEncoder<V> implements PrimitiveTypeEncoder<V> {
+
+    @Override
+    public boolean isArrayType() {
+        return false;
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        long writeSize = buffer.getWriteIndex() - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/DeliveryTagEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/DeliveryTagEncoder.java
new file mode 100644
index 0000000..3948dd5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/DeliveryTagEncoder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+
+/**
+ * Custom encoder for writing DeliveryTag types to a {@link ProtonBuffer}.
+ */
+public final class DeliveryTagEncoder implements TypeEncoder<DeliveryTag> {
+
+    @Override
+    public Class<DeliveryTag> getTypeClass() {
+        return DeliveryTag.class;
+    }
+
+    @Override
+    public boolean isArrayType() {
+        return false;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, DeliveryTag value) {
+        final int tagLength = value.tagLength();
+
+        if (tagLength > 255) {
+            buffer.writeByte(EncodingCodes.VBIN32);
+            buffer.writeInt(tagLength);
+        } else {
+            buffer.writeByte(EncodingCodes.VBIN8);
+            buffer.writeByte(tagLength);
+        }
+
+        value.writeTo(buffer);
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        throw new UnsupportedOperationException("Cannot Write Arrays of Delivery Tags, use Binary types instead.");
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        throw new UnsupportedOperationException("Cannot Write Arrays of Delivery Tags, use Binary types instead.");
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/PrimitiveTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/PrimitiveTypeEncoder.java
new file mode 100644
index 0000000..41c8cc5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/PrimitiveTypeEncoder.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+
+/**
+ * Marker interface for an encoder of Primitive types such as Integer or Boolean
+ *
+ * @param <V> the type that this encoder writes.
+ */
+public interface PrimitiveTypeEncoder<V> extends TypeEncoder<V> {
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoder.java
new file mode 100644
index 0000000..f9020dc
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoder.java
@@ -0,0 +1,767 @@
+/*
+* 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DescribedTypeEncoder;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.ArrayTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.BinaryTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.BooleanTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.ByteTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.CharacterTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.Decimal128TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.Decimal32TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.Decimal64TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.DoubleTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.FloatTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.IntegerTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.ListTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.LongTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.MapTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.NullTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.ShortTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.StringTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.SymbolTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.TimestampTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.UUIDTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.UnsignedByteTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.UnsignedIntegerTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.UnsignedLongTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.UnsignedShortTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.DescribedType;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * The default AMQP Encoder implementation.
+ */
+public final class ProtonEncoder implements Encoder {
+
+    // The encoders for primitives are fixed and cannot be altered by users who want
+    // to register custom encoders, these encoders are stateless so they can be safely
+    // made static to reduce overhead of creating and destroying this type.
+    private static final ArrayTypeEncoder arrayEncoder = new ArrayTypeEncoder();
+    private static final BinaryTypeEncoder binaryEncoder = new BinaryTypeEncoder();
+    private static final BooleanTypeEncoder booleanEncoder = new BooleanTypeEncoder();
+    private static final ByteTypeEncoder byteEncoder = new ByteTypeEncoder();
+    private static final CharacterTypeEncoder charEncoder = new CharacterTypeEncoder();
+    private static final Decimal32TypeEncoder decimal32Encoder = new Decimal32TypeEncoder();
+    private static final Decimal64TypeEncoder decimal64Encoder = new Decimal64TypeEncoder();
+    private static final Decimal128TypeEncoder decimal128Encoder = new Decimal128TypeEncoder();
+    private static final DoubleTypeEncoder doubleEncoder = new DoubleTypeEncoder();
+    private static final FloatTypeEncoder floatEncoder = new FloatTypeEncoder();
+    private static final IntegerTypeEncoder integerEncoder = new IntegerTypeEncoder();
+    private static final ListTypeEncoder listEncoder = new ListTypeEncoder();
+    private static final LongTypeEncoder longEncoder = new LongTypeEncoder();
+    private static final MapTypeEncoder mapEncoder = new MapTypeEncoder();
+    private static final NullTypeEncoder nullEncoder = new NullTypeEncoder();
+    private static final ShortTypeEncoder shortEncoder = new ShortTypeEncoder();
+    private static final StringTypeEncoder stringEncoder = new StringTypeEncoder();
+    private static final SymbolTypeEncoder symbolEncoder = new SymbolTypeEncoder();
+    private static final TimestampTypeEncoder timestampEncoder = new TimestampTypeEncoder();
+    private static final UnknownDescribedTypeEncoder unknownTypeEncoder = new UnknownDescribedTypeEncoder();
+    private static final UUIDTypeEncoder uuidEncoder = new UUIDTypeEncoder();
+    private static final UnsignedByteTypeEncoder ubyteEncoder = new UnsignedByteTypeEncoder();
+    private static final UnsignedShortTypeEncoder ushortEncoder = new UnsignedShortTypeEncoder();
+    private static final UnsignedIntegerTypeEncoder uintEncoder = new UnsignedIntegerTypeEncoder();
+    private static final UnsignedLongTypeEncoder ulongEncoder = new UnsignedLongTypeEncoder();
+    private static final DeliveryTagEncoder deliveryTagEncoder = new DeliveryTagEncoder();
+
+    private ProtonEncoderState singleThreadedState;
+
+    private final Map<Class<?>, TypeEncoder<?>> typeEncoders = new HashMap<>();
+    {
+        typeEncoders.put(arrayEncoder.getTypeClass(), arrayEncoder);
+        typeEncoders.put(binaryEncoder.getTypeClass(), binaryEncoder);
+        typeEncoders.put(booleanEncoder.getTypeClass(), booleanEncoder);
+        typeEncoders.put(byteEncoder.getTypeClass(), byteEncoder);
+        typeEncoders.put(charEncoder.getTypeClass(), charEncoder);
+        typeEncoders.put(decimal32Encoder.getTypeClass(), decimal32Encoder);
+        typeEncoders.put(decimal64Encoder.getTypeClass(), decimal64Encoder);
+        typeEncoders.put(decimal128Encoder.getTypeClass(), decimal128Encoder);
+        typeEncoders.put(doubleEncoder.getTypeClass(), doubleEncoder);
+        typeEncoders.put(floatEncoder.getTypeClass(), floatEncoder);
+        typeEncoders.put(integerEncoder.getTypeClass(), integerEncoder);
+        typeEncoders.put(listEncoder.getTypeClass(), listEncoder);
+        typeEncoders.put(longEncoder.getTypeClass(), longEncoder);
+        typeEncoders.put(mapEncoder.getTypeClass(), mapEncoder);
+        typeEncoders.put(nullEncoder.getTypeClass(), nullEncoder);
+        typeEncoders.put(shortEncoder.getTypeClass(), shortEncoder);
+        typeEncoders.put(stringEncoder.getTypeClass(), stringEncoder);
+        typeEncoders.put(symbolEncoder.getTypeClass(), symbolEncoder);
+        typeEncoders.put(timestampEncoder.getTypeClass(), timestampEncoder);
+        typeEncoders.put(unknownTypeEncoder.getTypeClass(), unknownTypeEncoder);
+        typeEncoders.put(uuidEncoder.getTypeClass(), uuidEncoder);
+        typeEncoders.put(ubyteEncoder.getTypeClass(), ubyteEncoder);
+        typeEncoders.put(ushortEncoder.getTypeClass(), ushortEncoder);
+        typeEncoders.put(uintEncoder.getTypeClass(), uintEncoder);
+        typeEncoders.put(ulongEncoder.getTypeClass(), ulongEncoder);
+        typeEncoders.put(deliveryTagEncoder.getTypeClass(), deliveryTagEncoder);
+    }
+
+    @Override
+    public ProtonEncoderState newEncoderState() {
+        return new ProtonEncoderState(this);
+    }
+
+    @Override
+    public ProtonEncoderState getCachedEncoderState() {
+        ProtonEncoderState state = singleThreadedState;
+        if (state == null) {
+            singleThreadedState = state = newEncoderState();
+        }
+
+        return singleThreadedState.reset();
+    }
+
+    @Override
+    public void writeNull(ProtonBuffer buffer, EncoderState state) throws EncodeException {
+        nullEncoder.writeType(buffer, state, null);
+    }
+
+    @Override
+    public void writeBoolean(ProtonBuffer buffer, EncoderState state, boolean value) throws EncodeException {
+        booleanEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeBoolean(ProtonBuffer buffer, EncoderState state, Boolean value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            buffer.writeByte(value ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+        }
+    }
+
+    @Override
+    public void writeUnsignedByte(ProtonBuffer buffer, EncoderState state, UnsignedByte value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            ubyteEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeUnsignedByte(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException {
+        ubyteEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeUnsignedShort(ProtonBuffer buffer, EncoderState state, UnsignedShort value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            ushortEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeUnsignedShort(ProtonBuffer buffer, EncoderState state, short value) throws EncodeException {
+        ushortEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeUnsignedShort(ProtonBuffer buffer, EncoderState state, int value) throws EncodeException {
+        if (value < 0) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            ushortEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, UnsignedInteger value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            uintEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException {
+        uintEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, int value) throws EncodeException {
+        uintEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeUnsignedInteger(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException {
+        if (value < 0) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            uintEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeUnsignedLong(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException {
+        ulongEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeUnsignedLong(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException {
+        ulongEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeUnsignedLong(ProtonBuffer buffer, EncoderState state, UnsignedLong value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            ulongEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeByte(ProtonBuffer buffer, EncoderState state, byte value) throws EncodeException {
+        byteEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeByte(ProtonBuffer buffer, EncoderState state, Byte value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            byteEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeShort(ProtonBuffer buffer, EncoderState state, short value) throws EncodeException {
+        shortEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeShort(ProtonBuffer buffer, EncoderState state, Short value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            shortEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeInteger(ProtonBuffer buffer, EncoderState state, int value) throws EncodeException {
+        integerEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeInteger(ProtonBuffer buffer, EncoderState state, Integer value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            integerEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeLong(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException {
+        longEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeLong(ProtonBuffer buffer, EncoderState state, Long value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            longEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeFloat(ProtonBuffer buffer, EncoderState state, float value) throws EncodeException {
+        floatEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeFloat(ProtonBuffer buffer, EncoderState state, Float value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            floatEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeDouble(ProtonBuffer buffer, EncoderState state, double value) throws EncodeException {
+        doubleEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeDouble(ProtonBuffer buffer, EncoderState state, Double value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            doubleEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeDecimal32(ProtonBuffer buffer, EncoderState state, Decimal32 value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            decimal32Encoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeDecimal64(ProtonBuffer buffer, EncoderState state, Decimal64 value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            decimal64Encoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeDecimal128(ProtonBuffer buffer, EncoderState state, Decimal128 value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            decimal128Encoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeCharacter(ProtonBuffer buffer, EncoderState state, char value) throws EncodeException {
+        charEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeCharacter(ProtonBuffer buffer, EncoderState state, Character value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            charEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeTimestamp(ProtonBuffer buffer, EncoderState state, long value) throws EncodeException {
+        timestampEncoder.writeType(buffer, state, value);
+    }
+
+    @Override
+    public void writeTimestamp(ProtonBuffer buffer, EncoderState state, Date value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            timestampEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeUUID(ProtonBuffer buffer, EncoderState state, UUID value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            uuidEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeBinary(ProtonBuffer buffer, EncoderState state, Binary value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            binaryEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeBinary(ProtonBuffer buffer, EncoderState state, ProtonBuffer value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            binaryEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeBinary(ProtonBuffer buffer, EncoderState state, byte[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            binaryEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeDeliveryTag(ProtonBuffer buffer, EncoderState state, DeliveryTag value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            deliveryTagEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeString(ProtonBuffer buffer, EncoderState state, String value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            stringEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeSymbol(ProtonBuffer buffer, EncoderState state, Symbol value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            symbolEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeSymbol(ProtonBuffer buffer, EncoderState state, String value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            symbolEncoder.writeType(buffer, state, Symbol.valueOf(value));
+        }
+    }
+
+    @Override
+    public <T> void writeList(ProtonBuffer buffer, EncoderState state, List<T> value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            listEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public <K, V> void writeMap(ProtonBuffer buffer, EncoderState state, Map<K, V> value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            mapEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeDescribedType(ProtonBuffer buffer, EncoderState state, DescribedType value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            unknownTypeEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, boolean[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, byte[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, short[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, int[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, long[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, float[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, double[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, char[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Decimal32[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Decimal64[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Decimal128[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Symbol[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedByte[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedShort[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedInteger[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, UnsignedLong[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, UUID[] value) throws EncodeException {
+        if (value == null) {
+            buffer.writeByte(EncodingCodes.NULL);
+        } else {
+            arrayEncoder.writeType(buffer, state, value);
+        }
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Override
+    public void writeObject(ProtonBuffer buffer, EncoderState state, Object value) throws EncodeException {
+        if (value != null) {
+            TypeEncoder encoder = typeEncoders.get(value.getClass());
+
+            if (encoder == null) {
+                writeUnregisteredType(buffer, state, value);
+            } else {
+                encoder.writeType(buffer, state, value);
+            }
+        } else {
+            buffer.writeByte(EncodingCodes.NULL);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void writeUnregisteredType(ProtonBuffer buffer, EncoderState state, Object value) {
+        if (value.getClass().isArray()) {
+            Class<?> componentType = value.getClass().getComponentType();
+            if (componentType.isPrimitive()) {
+                if (componentType == Boolean.TYPE) {
+                    writeArray(buffer, state, (boolean[]) value);
+                } else if (componentType == Byte.TYPE) {
+                    writeArray(buffer, state, (byte[]) value);
+                } else if (componentType == Short.TYPE) {
+                    writeArray(buffer, state, (short[]) value);
+                } else if (componentType == Integer.TYPE) {
+                    writeArray(buffer, state, (int[]) value);
+                } else if (componentType == Long.TYPE) {
+                    writeArray(buffer, state, (long[]) value);
+                } else if (componentType == Float.TYPE) {
+                    writeArray(buffer, state, (float[]) value);
+                } else if (componentType == Double.TYPE) {
+                    writeArray(buffer, state, (double[]) value);
+                } else if (componentType == Character.TYPE) {
+                    writeArray(buffer, state, (char[]) value);
+                } else {
+                    throw new IllegalArgumentException(
+                        "Cannot write arrays of type " + componentType.getName());
+                }
+            } else {
+                writeArray(buffer, state, (Object[]) value);
+            }
+        } else if (value instanceof List) {
+            writeList(buffer, state, (List<Object>) value);
+        } else if (value instanceof Map) {
+            writeMap(buffer, state, (Map<Object, Object>) value);
+        } else if (value instanceof DescribedType) {
+            writeDescribedType(buffer, state, (DescribedType) value);
+        } else {
+            throw new IllegalArgumentException(
+                "Do not know how to write Objects of class " + value.getClass().getName());
+        }
+    }
+
+    @Override
+    public <V> ProtonEncoder registerDescribedTypeEncoder(DescribedTypeEncoder<V> encoder) {
+        typeEncoders.put(encoder.getTypeClass(), encoder);
+        return this;
+    }
+
+    @Override
+    public TypeEncoder<?> getTypeEncoder(Object value) {
+        if (value == null) {
+            return nullEncoder;
+        } else {
+            return getTypeEncoder(value.getClass(), value);
+        }
+    }
+
+    @Override
+    public TypeEncoder<?> getTypeEncoder(Class<?> typeClass) {
+        return getTypeEncoder(typeClass, null);
+    }
+
+    public TypeEncoder<?> getTypeEncoder(Class<?> typeClass, Object instance) {
+        TypeEncoder<?> encoder = typeEncoders.get(typeClass);
+
+        if (encoder == null) {
+            encoder = deduceTypeEncoder(typeClass, instance);
+        }
+
+        return encoder;
+    }
+
+    private TypeEncoder<?> deduceTypeEncoder(Class<?> typeClass, Object instance) {
+        TypeEncoder<?> encoder = typeEncoders.get(typeClass);
+
+        if (typeClass.isArray()) {
+            encoder = arrayEncoder;
+        } else {
+            if (List.class.isAssignableFrom(typeClass)) {
+                encoder = listEncoder;
+            } else if (Map.class.isAssignableFrom(typeClass)) {
+                encoder = mapEncoder;
+            } else if (DescribedType.class.isAssignableFrom(typeClass)) {
+                // For instances of a specific DescribedType that we don't know about the
+                // generic described type encoder will work.  We don't use that though for
+                // class lookups as we don't want to allow arrays of polymorphic types.
+                if (encoder == null && instance != null) {
+                    if (encoder == null) {
+                        return unknownTypeEncoder;
+                    }
+                }
+            }
+        }
+
+        // Ensure that next time we find the encoder immediately and don't need to
+        // go through this process again.
+        typeEncoders.put(typeClass, encoder);
+
+        return encoder;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderFactory.java
new file mode 100644
index 0000000..0d31f89
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderFactory.java
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.codec.encoders.messaging.AcceptedTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.AmqpSequenceTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.AmqpValueTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ApplicationPropertiesTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DataTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnCloseTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnNoLinksOrMessagesTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnNoLinksTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnNoMessagesTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeliveryAnnotationsTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.FooterTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.HeaderTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.MessageAnnotationsTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ModifiedTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.PropertiesTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ReceivedTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.RejectedTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ReleasedTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.SourceTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.TargetTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslChallengeTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslInitTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslMechanismsTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslOutcomeTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslResponseTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.CoordinatorTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.DeclareTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.DeclaredTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.DischargeTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.TransactionStateTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.AttachTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.BeginTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.CloseTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.DetachTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.DispositionTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.EndTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.ErrorConditionTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.FlowTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.OpenTypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.TransferTypeEncoder;
+
+/**
+ * Factory that create and initializes new BuiltinEncoder instances
+ */
+public final class ProtonEncoderFactory {
+
+    private ProtonEncoderFactory() {
+    }
+
+    public static ProtonEncoder create() {
+        ProtonEncoder encoder = new ProtonEncoder();
+
+        addMessagingTypeEncoders(encoder);
+        addTransactionTypeEncoders(encoder);
+        addTransportTypeEncoders(encoder);
+
+        return encoder;
+    }
+
+    public static ProtonEncoder createSasl() {
+        ProtonEncoder encoder = new ProtonEncoder();
+
+        addSaslTypeEncoders(encoder);
+
+        return encoder;
+    }
+
+    private static void addMessagingTypeEncoders(ProtonEncoder encoder) {
+        encoder.registerDescribedTypeEncoder(new AcceptedTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new AmqpSequenceTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new AmqpValueTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new ApplicationPropertiesTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DataTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeleteOnCloseTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeleteOnNoLinksOrMessagesTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeleteOnNoLinksTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeleteOnNoMessagesTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeliveryAnnotationsTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new FooterTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new HeaderTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new MessageAnnotationsTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new ModifiedTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new PropertiesTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new ReceivedTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new RejectedTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new ReleasedTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new SourceTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new TargetTypeEncoder());
+    }
+
+    private static void addTransactionTypeEncoders(ProtonEncoder encoder) {
+        encoder.registerDescribedTypeEncoder(new CoordinatorTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeclaredTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DeclareTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DischargeTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new TransactionStateTypeEncoder());
+    }
+
+    private static void addTransportTypeEncoders(ProtonEncoder encoder) {
+        encoder.registerDescribedTypeEncoder(new AttachTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new BeginTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new CloseTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DetachTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new DispositionTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new EndTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new ErrorConditionTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new FlowTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new OpenTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new TransferTypeEncoder());
+    }
+
+    private static void addSaslTypeEncoders(ProtonEncoder encoder) {
+        encoder.registerDescribedTypeEncoder(new SaslChallengeTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new SaslInitTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new SaslMechanismsTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new SaslOutcomeTypeEncoder());
+        encoder.registerDescribedTypeEncoder(new SaslResponseTypeEncoder());
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderState.java
new file mode 100644
index 0000000..08e4c19
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderState.java
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+
+/**
+ * State object used by the Built in Encoder implementation.
+ */
+public final class ProtonEncoderState implements EncoderState {
+
+    private final ProtonEncoder encoder;
+
+    private UTF8Encoder utf8Encoder;
+
+    public ProtonEncoderState(ProtonEncoder encoder) {
+        this.encoder = encoder;
+    }
+
+    @Override
+    public ProtonEncoder getEncoder() {
+        return this.encoder;
+    }
+
+    public UTF8Encoder getUTF8Encoder() {
+        return utf8Encoder;
+    }
+
+    public void setUTF8Encoder(UTF8Encoder utf8Encoder) {
+        this.utf8Encoder = utf8Encoder;
+    }
+
+    @Override
+    public ProtonEncoderState reset() {
+        // No intermediate state to reset
+        return this;
+    }
+
+    @Override
+    public ProtonBuffer encodeUTF8(ProtonBuffer buffer, CharSequence sequence) {
+        if (utf8Encoder == null) {
+            encodeUTF8Sequence(buffer, sequence);
+        } else {
+            utf8Encoder.encodeUTF8(buffer, sequence);
+        }
+
+        return buffer;
+    }
+
+    private static void encodeUTF8Sequence(ProtonBuffer buffer, CharSequence sequence) {
+        final int length = sequence.length();
+
+        int position = buffer.getWriteIndex();
+        int index = 0;
+        int ch = 0;
+
+        // Assume ASCII and just reserve what we need for that case.
+        buffer.ensureWritable(length);
+
+        // ASCII Optimized path U+0000..U+007F
+        for (; index < length && (ch = sequence.charAt(index)) < 0x80; ++index) {
+            buffer.setByte(position++, (byte) ch);
+        }
+
+        if (index < length) {
+            // Non-ASCII path
+            position = extendedEncodeUTF8Sequence(buffer, sequence, index, position);
+        }
+
+        buffer.setWriteIndex(position);
+    }
+
+    private static int extendedEncodeUTF8Sequence(ProtonBuffer buffer, CharSequence value, int index, int position) {
+        // Size buffer to what we know we will need to complete this encode.
+        buffer.ensureWritable(calculateUTF8Length(index, value));
+
+        int remaining = value.length();
+
+        for (int i = index; i < remaining; i++) {
+            int c = value.charAt(i);
+            if ((c & 0xFF80) == 0) {
+                // U+0000..U+007F
+                buffer.setByte(position++, (byte) c);
+            } else if ((c & 0xF800) == 0) {
+                // U+0080..U+07FF
+                buffer.setByte(position++, (byte)(0xC0 | ((c >> 6) & 0x1F)));
+                buffer.setByte(position++, (byte)(0x80 | (c & 0x3F)));
+            } else if ((c & 0xD800) != 0xD800 || (c > 0xDBFF)) {
+                // U+0800..U+FFFF - excluding surrogate pairs
+                buffer.setByte(position++, (byte)(0xE0 | ((c >> 12) & 0x0F)));
+                buffer.setByte(position++, (byte)(0x80 | ((c >> 6) & 0x3F)));
+                buffer.setByte(position++, (byte)(0x80 | (c & 0x3F)));
+            } else {
+                int low;
+
+                if ((++i == remaining) || ((low = value.charAt(i)) & 0xDC00) != 0xDC00) {
+                    throw new IllegalArgumentException("String contains invalid Unicode code points");
+                }
+
+                c = 0x010000 + ((c & 0x03FF) << 10) + (low & 0x03FF);
+
+                buffer.setByte(position++, (byte)(0xF0 | ((c >> 18) & 0x07)));
+                buffer.setByte(position++, (byte)(0x80 | ((c >> 12) & 0x3F)));
+                buffer.setByte(position++, (byte)(0x80 | ((c >> 6) & 0x3F)));
+                buffer.setByte(position++, (byte)(0x80 | (c & 0x3F)));
+            }
+        }
+
+        return position;
+    }
+
+    private static int calculateUTF8Length(int startPos, final CharSequence sequence) {
+        int encodedSize = sequence.length();
+        final int length = encodedSize;
+
+        for (int i = 0; i < length; i++) {
+            int c = sequence.charAt(i);
+
+            // U+0080..
+            if ((c & 0xFF80) != 0) {
+                encodedSize++;
+
+                // U+0800..
+                if(((c & 0xF800) != 0)) {
+                    encodedSize++;
+
+                    // surrogate pairs should always combine to create a code point
+                    // with a 4 octet representation
+                    if ((c & 0xD800) == 0xD800 && c < 0xDC00) {
+                        i++;
+                    }
+                }
+            }
+        }
+
+        return encodedSize;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/UTF8Encoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/UTF8Encoder.java
new file mode 100644
index 0000000..a3600b6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/UTF8Encoder.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Interface for an external UTF8 Encoder that can be supplied by a client
+ * which implements custom encoding logic optimized for the application using
+ * the Codec.
+ */
+public interface UTF8Encoder {
+
+    /**
+     * Encodes the given sequence of characters in UTF8 to the given buffer.
+     *
+     * @param buffer
+     *      A ProtonBuffer where the UTF-8 encoded bytes should be written.
+     * @param sequence
+     *      A {@link CharSequence} representing the UTF-8 bytes to encode
+     *
+     * @return a reference to the encoding buffer for chaining
+     */
+    ProtonBuffer encodeUTF8(ProtonBuffer buffer, CharSequence sequence);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/UnknownDescribedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/UnknownDescribedTypeEncoder.java
new file mode 100644
index 0000000..62c0492
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/UnknownDescribedTypeEncoder.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.types.DescribedType;
+
+/**
+ * Encoder of AMQP Described Types to a byte stream.
+ */
+public final class UnknownDescribedTypeEncoder implements TypeEncoder<DescribedType> {
+
+    @Override
+    public Class<DescribedType> getTypeClass() {
+        return DescribedType.class;
+    }
+
+    @Override
+    public boolean isArrayType() {
+        return false;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, DescribedType value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeObject(buffer, state, value.getDescriptor());
+        state.getEncoder().writeObject(buffer, state, value.getDescribed());
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] value) {
+        throw new UnsupportedOperationException("Cannot write array of unknown described types.");
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        throw new UnsupportedOperationException("Cannot write array of unknown described types.");
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AcceptedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AcceptedTypeEncoder.java
new file mode 100644
index 0000000..ea6282d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AcceptedTypeEncoder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+
+/**
+ * Encoder of AMQP Accepted type values to a byte stream
+ */
+public final class AcceptedTypeEncoder extends AbstractDescribedListTypeEncoder<Accepted> {
+
+    @Override
+    public Class<Accepted> getTypeClass() {
+        return Accepted.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Accepted.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Accepted.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public byte getListEncoding(Accepted value) {
+        return EncodingCodes.LIST0 & 0xff;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Accepted value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST0);
+    }
+
+    @Override
+    public void writeElement(Accepted source, int index, ProtonBuffer buffer, EncoderState state) {
+    }
+
+    @Override
+    public int getElementCount(Accepted value) {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AmqpSequenceTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AmqpSequenceTypeEncoder.java
new file mode 100644
index 0000000..e896c0c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AmqpSequenceTypeEncoder.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+
+/**
+ * Encoder of AMQP AmqpSequence type values to a byte stream.
+ */
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public final class AmqpSequenceTypeEncoder extends AbstractDescribedTypeEncoder<AmqpSequence> {
+
+    @Override
+    public Class<AmqpSequence> getTypeClass() {
+        return AmqpSequence.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return AmqpSequence.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return AmqpSequence.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, AmqpSequence value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(AmqpSequence.DESCRIPTOR_CODE.byteValue());
+
+        state.getEncoder().writeList(buffer, state, value.getValue());
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+
+        List[] elements = new List[values.length];
+
+        for (int i = 0; i < values.length; ++i) {
+            AmqpSequence sequence = (AmqpSequence) values[i];
+            elements[i] = sequence.getValue();
+        }
+
+        TypeEncoder<?> entryEncoder = state.getEncoder().getTypeEncoder(List.class);
+        entryEncoder.writeRawArray(buffer, state, elements);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AmqpValueTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AmqpValueTypeEncoder.java
new file mode 100644
index 0000000..08783c5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/AmqpValueTypeEncoder.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+
+/**
+ * Encoder of AMQP Value type values to a byte stream.
+ */
+@SuppressWarnings({ "rawtypes" })
+public final class AmqpValueTypeEncoder extends AbstractDescribedTypeEncoder<AmqpValue> {
+
+    @Override
+    public Class<AmqpValue> getTypeClass() {
+        return AmqpValue.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return AmqpValue.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return AmqpValue.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, AmqpValue value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(AmqpValue.DESCRIPTOR_CODE.byteValue());
+
+        state.getEncoder().writeObject(buffer, state, value.getValue());
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+
+        Object[] elements = new Object[values.length];
+
+        for (int i = 0; i < values.length; ++i) {
+            AmqpValue value = (AmqpValue) values[i];
+            elements[i] = value.getValue();
+        }
+
+        TypeEncoder<?> entryEncoder = state.getEncoder().getTypeEncoder(elements[0].getClass());
+
+        // This should fail if the array of AmqpValue do not all contain the same type
+        // in the value portion of the sequence.
+        entryEncoder.writeRawArray(buffer, state, elements);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ApplicationPropertiesTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ApplicationPropertiesTypeEncoder.java
new file mode 100644
index 0000000..912a6ef
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ApplicationPropertiesTypeEncoder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedMapTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+
+/**
+ * Encoder of AMQP ApplicationProperties type values to a byte stream.
+ */
+public final class ApplicationPropertiesTypeEncoder extends AbstractDescribedMapTypeEncoder<String, Object, ApplicationProperties> {
+
+    @Override
+    public Class<ApplicationProperties> getTypeClass() {
+        return ApplicationProperties.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return ApplicationProperties.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return ApplicationProperties.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public boolean hasMap(ApplicationProperties value) {
+        return value.getValue() != null;
+    }
+
+    @Override
+    public int getMapSize(ApplicationProperties value) {
+        if (value.getValue() != null) {
+            return value.getValue().size();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void writeMapEntries(ProtonBuffer buffer, EncoderState state, ApplicationProperties value) {
+        // Write the Map elements and then compute total size written.
+        for (Map.Entry<String, Object> entry : value.getValue().entrySet()) {
+            state.getEncoder().writeString(buffer, state, entry.getKey());
+            state.getEncoder().writeObject(buffer, state, entry.getValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DataTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DataTypeEncoder.java
new file mode 100644
index 0000000..a6ec328
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DataTypeEncoder.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Data;
+
+/**
+ * Encoder of AMQP Data type values to a byte stream.
+ */
+public final class DataTypeEncoder extends AbstractDescribedTypeEncoder<Data> {
+
+    @Override
+    public Class<Data> getTypeClass() {
+        return Data.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Data.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Data.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Data value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+
+        state.getEncoder().writeBinary(buffer, state, value.getValue());
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        final int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        final int endIndex = buffer.getWriteIndex();
+        final long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        for (Object value : values) {
+            final Binary binary = ((Data) value).getBinary();
+            buffer.writeInt(binary.getLength());
+            buffer.writeBytes(binary.getArray(), binary.getArrayOffset(), binary.getLength());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnCloseTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnCloseTypeEncoder.java
new file mode 100644
index 0000000..7108574
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnCloseTypeEncoder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnClose;
+
+/**
+ * Encoder of AMQP DeleteOnClose type values to a byte stream.
+ */
+public final class DeleteOnCloseTypeEncoder extends AbstractDescribedListTypeEncoder<DeleteOnClose> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnClose.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnClose.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<DeleteOnClose> getTypeClass() {
+        return DeleteOnClose.class;
+    }
+
+    @Override
+    public byte getListEncoding(DeleteOnClose value) {
+        return EncodingCodes.LIST0 & 0xff;
+    }
+
+    @Override
+    public void writeElement(DeleteOnClose source, int index, ProtonBuffer buffer, EncoderState state) {
+    }
+
+    @Override
+    public int getElementCount(DeleteOnClose value) {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoLinksOrMessagesTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoLinksOrMessagesTypeEncoder.java
new file mode 100644
index 0000000..396b441
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoLinksOrMessagesTypeEncoder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinksOrMessages;
+
+/**
+ * Encoder of AMQP DeleteOnNoLinksOrMessages type values to a byte stream
+ */
+public final class DeleteOnNoLinksOrMessagesTypeEncoder extends AbstractDescribedListTypeEncoder<DeleteOnNoLinksOrMessages> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnNoLinksOrMessages.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<DeleteOnNoLinksOrMessages> getTypeClass() {
+        return DeleteOnNoLinksOrMessages.class;
+    }
+
+    @Override
+    public byte getListEncoding(DeleteOnNoLinksOrMessages value) {
+        return EncodingCodes.LIST0 & 0xff;
+    }
+
+    @Override
+    public void writeElement(DeleteOnNoLinksOrMessages source, int index, ProtonBuffer buffer, EncoderState state) {
+    }
+
+    @Override
+    public int getElementCount(DeleteOnNoLinksOrMessages value) {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoLinksTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoLinksTypeEncoder.java
new file mode 100644
index 0000000..238845d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoLinksTypeEncoder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinks;
+
+/**
+ * Encoder of AMQP DeleteOnNoLinks type values to a byte stream
+ */
+public final class DeleteOnNoLinksTypeEncoder extends AbstractDescribedListTypeEncoder<DeleteOnNoLinks> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnNoLinks.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnNoLinks.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<DeleteOnNoLinks> getTypeClass() {
+        return DeleteOnNoLinks.class;
+    }
+
+    @Override
+    public byte getListEncoding(DeleteOnNoLinks value) {
+        return EncodingCodes.LIST0 & 0xff;
+    }
+
+    @Override
+    public void writeElement(DeleteOnNoLinks source, int index, ProtonBuffer buffer, EncoderState state) {
+    }
+
+    @Override
+    public int getElementCount(DeleteOnNoLinks value) {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoMessagesTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoMessagesTypeEncoder.java
new file mode 100644
index 0000000..c8dda58
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeleteOnNoMessagesTypeEncoder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoMessages;
+
+/**
+ * Encoder of AMQP DeleteOnNoMessages type values to a byte stream
+ */
+public final class DeleteOnNoMessagesTypeEncoder extends AbstractDescribedListTypeEncoder<DeleteOnNoMessages> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeleteOnNoMessages.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeleteOnNoMessages.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<DeleteOnNoMessages> getTypeClass() {
+        return DeleteOnNoMessages.class;
+    }
+
+    @Override
+    public byte getListEncoding(DeleteOnNoMessages value) {
+        return EncodingCodes.LIST0 & 0xff;
+    }
+
+    @Override
+    public void writeElement(DeleteOnNoMessages source, int index, ProtonBuffer buffer, EncoderState state) {
+    }
+
+    @Override
+    public int getElementCount(DeleteOnNoMessages value) {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeliveryAnnotationsTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeliveryAnnotationsTypeEncoder.java
new file mode 100644
index 0000000..774f8e6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/DeliveryAnnotationsTypeEncoder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedMapTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+
+/**
+ * Encoder of AMQP DeliveryAnnotations type values to a byte stream.
+ */
+public final class DeliveryAnnotationsTypeEncoder extends AbstractDescribedMapTypeEncoder<Symbol, Object, DeliveryAnnotations> {
+
+    @Override
+    public Class<DeliveryAnnotations> getTypeClass() {
+        return DeliveryAnnotations.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return DeliveryAnnotations.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return DeliveryAnnotations.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public boolean hasMap(DeliveryAnnotations value) {
+        return value.getValue() != null;
+    }
+
+    @Override
+    public int getMapSize(DeliveryAnnotations value) {
+        if (value.getValue() != null) {
+            return value.getValue().size();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void writeMapEntries(ProtonBuffer buffer, EncoderState state, DeliveryAnnotations value) {
+        // Write the Map elements and then compute total size written.
+        for (Map.Entry<Symbol, Object> entry : value.getValue().entrySet()) {
+            state.getEncoder().writeSymbol(buffer, state, entry.getKey());
+            state.getEncoder().writeObject(buffer, state, entry.getValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/FooterTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/FooterTypeEncoder.java
new file mode 100644
index 0000000..5bb2fd2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/FooterTypeEncoder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedMapTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+
+/**
+ * Encoder of AMQP Footer type values to a byte stream
+ */
+public final class FooterTypeEncoder extends AbstractDescribedMapTypeEncoder<Object, Object, Footer> {
+
+    @Override
+    public Class<Footer> getTypeClass() {
+        return Footer.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Footer.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Footer.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public boolean hasMap(Footer value) {
+        return value.getValue() != null;
+    }
+
+    @Override
+    public int getMapSize(Footer value) {
+        if (value.getValue() != null) {
+            return value.getValue().size();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void writeMapEntries(ProtonBuffer buffer, EncoderState state, Footer value) {
+        // Write the Map elements and then compute total size written.
+        for (Map.Entry<Symbol, Object> entry : value.getValue().entrySet()) {
+            state.getEncoder().writeObject(buffer, state, entry.getKey());
+            state.getEncoder().writeObject(buffer, state, entry.getValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/HeaderTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/HeaderTypeEncoder.java
new file mode 100644
index 0000000..6bcdf62
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/HeaderTypeEncoder.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Header;
+
+/**
+ * Encoder of AMQP Header type values to a byte stream
+ */
+public final class HeaderTypeEncoder extends AbstractDescribedListTypeEncoder<Header> {
+
+    @Override
+    public Class<Header> getTypeClass() {
+        return Header.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Header.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Header.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public byte getListEncoding(Header value) {
+        return EncodingCodes.LIST8;
+    }
+
+    @Override
+    public void writeElement(Header header, int index, ProtonBuffer buffer, EncoderState state) {
+        // When encoding ensure that values that were never set are omitted and a simple
+        // NULL entry is written in the slot instead (don't write defaults).
+
+        switch (index) {
+            case 0:
+                if (header.hasDurable()) {
+                    buffer.writeByte(header.isDurable() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                if (header.hasPriority()) {
+                    state.getEncoder().writeUnsignedByte(buffer, state, header.getPriority());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 2:
+                if (header.hasTimeToLive()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, header.getTimeToLive());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 3:
+                if (header.hasFirstAcquirer()) {
+                    buffer.writeByte(header.isFirstAcquirer() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 4:
+                if (header.hasDeliveryCount()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, header.getDeliveryCount());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Header value index: " + index);
+        }
+    }
+
+    @Override
+    public int getElementCount(Header header) {
+        return header.getElementCount();
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/MessageAnnotationsTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/MessageAnnotationsTypeEncoder.java
new file mode 100644
index 0000000..b4233aa
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/MessageAnnotationsTypeEncoder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedMapTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+
+/**
+ * Encoder of AMQP MessageAnnotations type values to a byte stream.
+ */
+public final class MessageAnnotationsTypeEncoder extends AbstractDescribedMapTypeEncoder<Symbol, Object, MessageAnnotations> {
+
+    @Override
+    public Class<MessageAnnotations> getTypeClass() {
+        return MessageAnnotations.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return MessageAnnotations.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return MessageAnnotations.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public boolean hasMap(MessageAnnotations value) {
+        return value.getValue() != null;
+    }
+
+    @Override
+    public int getMapSize(MessageAnnotations value) {
+        if (value.getValue() != null) {
+            return value.getValue().size();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void writeMapEntries(ProtonBuffer buffer, EncoderState state, MessageAnnotations value) {
+        // Write the Map elements and then compute total size written.
+        for (Map.Entry<Symbol, Object> entry : value.getValue().entrySet()) {
+            state.getEncoder().writeSymbol(buffer, state, entry.getKey());
+            state.getEncoder().writeObject(buffer, state, entry.getValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ModifiedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ModifiedTypeEncoder.java
new file mode 100644
index 0000000..c8eb03e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ModifiedTypeEncoder.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+
+/**
+ * Encoder of AMQP Modified type values to a byte stream.
+ */
+public final class ModifiedTypeEncoder extends AbstractDescribedListTypeEncoder<Modified> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Modified.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Modified.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Modified> getTypeClass() {
+        return Modified.class;
+    }
+
+    @Override
+    public void writeElement(Modified source, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                buffer.writeByte(source.isDeliveryFailed() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                break;
+            case 1:
+                buffer.writeByte(source.isUndeliverableHere() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                break;
+            case 2:
+                state.getEncoder().writeMap(buffer, state, source.getMessageAnnotations());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Modified value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Modified value) {
+        if (value.getMessageAnnotations() != null) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST8;
+        }
+    }
+
+    @Override
+    public int getElementCount(Modified value) {
+        if (value.getMessageAnnotations() != null) {
+            return 3;
+        } else if (value.isUndeliverableHere()) {
+            return 2;
+        } else if (value.isDeliveryFailed()) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/PropertiesTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/PropertiesTypeEncoder.java
new file mode 100644
index 0000000..d95bc2d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/PropertiesTypeEncoder.java
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+
+/**
+ * Encoder of AMQP Properties type value to a byte stream.
+ */
+public final class PropertiesTypeEncoder extends AbstractDescribedListTypeEncoder<Properties> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Properties.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Properties.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Properties> getTypeClass() {
+        return Properties.class;
+    }
+
+    @Override
+    public void writeElement(Properties properties, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeObject(buffer, state, properties.getMessageId());
+                break;
+            case 1:
+                state.getEncoder().writeBinary(buffer, state, properties.getUserId());
+                break;
+            case 2:
+                state.getEncoder().writeString(buffer, state, properties.getTo());
+                break;
+            case 3:
+                state.getEncoder().writeString(buffer, state, properties.getSubject());
+                break;
+            case 4:
+                state.getEncoder().writeString(buffer, state, properties.getReplyTo());
+                break;
+            case 5:
+                state.getEncoder().writeObject(buffer, state, properties.getCorrelationId());
+                break;
+            case 6:
+                state.getEncoder().writeSymbol(buffer, state, properties.getContentType());
+                break;
+            case 7:
+                state.getEncoder().writeSymbol(buffer, state, properties.getContentEncoding());
+                break;
+            case 8:
+                if (properties.hasAbsoluteExpiryTime()) {
+                    state.getEncoder().writeTimestamp(buffer, state, properties.getAbsoluteExpiryTime());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 9:
+                if (properties.hasCreationTime()) {
+                    state.getEncoder().writeTimestamp(buffer, state, properties.getCreationTime());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 10:
+                state.getEncoder().writeString(buffer, state, properties.getGroupId());
+                break;
+            case 11:
+                if (properties.hasGroupSequence()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, properties.getGroupSequence());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 12:
+                state.getEncoder().writeString(buffer, state, properties.getReplyToGroupId());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Properties value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Properties value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Properties properties) {
+        return properties.getElementCount();
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ReceivedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ReceivedTypeEncoder.java
new file mode 100644
index 0000000..c2b8161
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ReceivedTypeEncoder.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Received;
+
+/**
+ * Encoder of AMQP Received type values from a byte stream.
+ */
+public final class ReceivedTypeEncoder extends AbstractDescribedListTypeEncoder<Received> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Received.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Received.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Received> getTypeClass() {
+        return Received.class;
+    }
+
+    @Override
+    public void writeElement(Received source, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeUnsignedInteger(buffer, state, source.getSectionNumber());
+                break;
+            case 1:
+                state.getEncoder().writeUnsignedLong(buffer, state, source.getSectionOffset());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Received value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Received value) {
+        return EncodingCodes.LIST8;
+    }
+
+    @Override
+    public int getElementCount(Received value) {
+        if (value.getSectionOffset() != null) {
+            return 2;
+        } else if (value.getSectionNumber() != null) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 2;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/RejectedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/RejectedTypeEncoder.java
new file mode 100644
index 0000000..39dd8d4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/RejectedTypeEncoder.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+
+/**
+ * Encoder of AMQP Rejected type values to a byte stream.
+ */
+public final class RejectedTypeEncoder extends AbstractDescribedListTypeEncoder<Rejected> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Rejected.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Rejected.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Rejected> getTypeClass() {
+        return Rejected.class;
+    }
+
+    @Override
+    public void writeElement(Rejected source, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeObject(buffer, state, source.getError());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Rejected value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Rejected value) {
+        if (value.getError() != null) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST8;
+        }
+    }
+
+    @Override
+    public int getElementCount(Rejected value) {
+        if (value.getError() != null) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ReleasedTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ReleasedTypeEncoder.java
new file mode 100644
index 0000000..be1258e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/ReleasedTypeEncoder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Released;
+
+/**
+ * Encoder of AMQP Released type values to a byte stream
+ */
+public final class ReleasedTypeEncoder extends AbstractDescribedListTypeEncoder<Released> {
+
+    @Override
+    public Class<Released> getTypeClass() {
+        return Released.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Released.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Released.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public byte getListEncoding(Released value) {
+        return EncodingCodes.LIST0;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Released value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Released.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST0);
+    }
+
+    @Override
+    public void writeElement(Released source, int index, ProtonBuffer buffer, EncoderState state) {
+    }
+
+    @Override
+    public int getElementCount(Released value) {
+        return 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/SourceTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/SourceTypeEncoder.java
new file mode 100644
index 0000000..da304bf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/SourceTypeEncoder.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+
+/**
+ * Encoder of AMQP Source type values to a byte stream.
+ */
+public final class SourceTypeEncoder extends AbstractDescribedListTypeEncoder<Source> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Source.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Source.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Source> getTypeClass() {
+        return Source.class;
+    }
+
+    @Override
+    public void writeElement(Source source, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeString(buffer, state, source.getAddress());
+                break;
+            case 1:
+                state.getEncoder().writeUnsignedInteger(buffer, state, source.getDurable().getValue());
+                break;
+            case 2:
+                state.getEncoder().writeSymbol(buffer, state, source.getExpiryPolicy().getPolicy());
+                break;
+            case 3:
+                state.getEncoder().writeUnsignedInteger(buffer, state, source.getTimeout());
+                break;
+            case 4:
+                buffer.writeByte(source.isDynamic() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                break;
+            case 5:
+                state.getEncoder().writeMap(buffer, state, source.getDynamicNodeProperties());
+                break;
+            case 6:
+                state.getEncoder().writeSymbol(buffer, state, source.getDistributionMode());
+                break;
+            case 7:
+                state.getEncoder().writeMap(buffer, state, source.getFilter());
+                break;
+            case 8:
+                state.getEncoder().writeObject(buffer, state, source.getDefaultOutcome());
+                break;
+            case 9:
+                state.getEncoder().writeArray(buffer, state, source.getOutcomes());
+                break;
+            case 10:
+                state.getEncoder().writeArray(buffer, state, source.getCapabilities());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Source value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Source value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Source source) {
+        if (source.getCapabilities() != null) {
+            return 11;
+        } else if (source.getOutcomes() != null) {
+            return 10;
+        } else if (source.getDefaultOutcome() != null) {
+            return 9;
+        } else if (source.getFilter() != null) {
+            return 8;
+        } else if (source.getDistributionMode() != null) {
+            return 7;
+        } else if (source.getDynamicNodeProperties() != null) {
+            return 6;
+        } else if (source.isDynamic()) {
+            return 5;
+        } else if (source.getTimeout() != null && !source.getTimeout().equals(UnsignedInteger.ZERO)) {
+            return 4;
+        } else if (source.getExpiryPolicy() != null && source.getExpiryPolicy() != TerminusExpiryPolicy.SESSION_END) {
+            return 3;
+        } else if (source.getDurable() != null && source.getDurable() != TerminusDurability.NONE) {
+            return 2;
+        } else if (source.getAddress() != null) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/TargetTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/TargetTypeEncoder.java
new file mode 100644
index 0000000..c500037
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/messaging/TargetTypeEncoder.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+
+/**
+ * Encoder of AMQP Target type values to a byte stream.
+ */
+public final class TargetTypeEncoder extends AbstractDescribedListTypeEncoder<Target> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Target.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Target.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Target> getTypeClass() {
+        return Target.class;
+    }
+
+    @Override
+    public void writeElement(Target target, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeString(buffer, state, target.getAddress());
+                break;
+            case 1:
+                state.getEncoder().writeUnsignedInteger(buffer, state, target.getDurable().getValue());
+                break;
+            case 2:
+                state.getEncoder().writeSymbol(buffer, state, target.getExpiryPolicy().getPolicy());
+                break;
+            case 3:
+                state.getEncoder().writeUnsignedInteger(buffer, state, target.getTimeout());
+                break;
+            case 4:
+                buffer.writeByte(target.isDynamic() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                break;
+            case 5:
+                state.getEncoder().writeMap(buffer, state, target.getDynamicNodeProperties());
+                break;
+            case 6:
+                state.getEncoder().writeArray(buffer, state, target.getCapabilities());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Target value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Target value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Target target) {
+        if (target.getCapabilities() != null) {
+            return 7;
+        } else if (target.getDynamicNodeProperties() != null) {
+            return 6;
+        } else if (target.isDynamic()) {
+            return 5;
+        } else if (target.getTimeout() != null && !target.getTimeout().equals(UnsignedInteger.ZERO)) {
+            return 4;
+        } else if (target.getExpiryPolicy() != null && target.getExpiryPolicy() != TerminusExpiryPolicy.SESSION_END) {
+            return 3;
+        } else if (target.getDurable() != null && target.getDurable() != TerminusDurability.NONE) {
+            return 2;
+        } else if (target.getAddress() != null) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ArrayTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ArrayTypeEncoder.java
new file mode 100644
index 0000000..f98d7ad
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ArrayTypeEncoder.java
@@ -0,0 +1,275 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.PrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Array types to a byte stream.
+ */
+public final class ArrayTypeEncoder implements PrimitiveTypeEncoder<Object> {
+
+    @Override
+    public boolean isArrayType() {
+        return true;
+    }
+
+    @Override
+    public Class<Object> getTypeClass() {
+        return Object.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Object value) {
+        if (!value.getClass().isArray()) {
+            throw new IllegalArgumentException("Expected Array type but got: " + value.getClass().getSimpleName());
+        }
+
+        Class<?> componentType = value.getClass().getComponentType();
+        if (componentType.isPrimitive()) {
+            if (componentType == Boolean.TYPE) {
+                writeType(buffer, state, (boolean[]) value);
+            } else if (componentType == Byte.TYPE) {
+                writeType(buffer, state, (byte[]) value);
+            } else if (componentType == Short.TYPE) {
+                writeType(buffer, state, (short[]) value);
+            } else if (componentType == Integer.TYPE) {
+                writeType(buffer, state, (int[]) value);
+            } else if (componentType == Long.TYPE) {
+                writeType(buffer, state, (long[]) value);
+            } else if (componentType == Float.TYPE) {
+                writeType(buffer, state, (float[]) value);
+            } else if (componentType == Double.TYPE) {
+                writeType(buffer, state, (double[]) value);
+            } else if (componentType == Character.TYPE) {
+                writeType(buffer, state, (char[]) value);
+            } else {
+                throw new IllegalArgumentException(
+                    "Cannot write arrays of type " + componentType.getName());
+            }
+        } else {
+            writeArray(buffer, state, (Object[]) value);
+        }
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        TypeEncoder<?> typeEncoder = findTypeEncoder(buffer, state, values);
+
+        // If the is an array of arrays then we need to control the encoding code
+        // and size indicators and hand off writing the entries to the raw write
+        // method which will follow the nested path.
+        if (typeEncoder.isArrayType()) {
+            // Write the Array Type encoding code, we don't optimize here.
+            buffer.writeByte(EncodingCodes.ARRAY32);
+
+            int startIndex = buffer.getWriteIndex();
+
+            // Reserve space for the size and write the count of list elements.
+            buffer.writeInt(0);
+            buffer.writeInt(values.length);
+
+            // Write the arrays as a raw series of arrays accounting for nested arrays
+            writeRawArray(buffer, state, values);
+
+            // Move back and write the size
+            long writeSize = buffer.getWriteIndex() - startIndex - Integer.BYTES;
+
+            if (writeSize > Integer.MAX_VALUE) {
+                throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+            }
+
+            buffer.setInt(startIndex, (int) writeSize);
+        } else {
+            typeEncoder.writeArray(buffer, state, values);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        TypeEncoder<?> typeEncoder = findTypeEncoder(buffer, state, values[0]);
+
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        for (int i = 0; i < values.length; ++i) {
+            int startIndex = buffer.getWriteIndex();
+
+            // Reserve space for the size and write the count of list elements.
+            buffer.writeInt(0);
+
+            typeEncoder = findTypeEncoder(buffer, state, values[i]);
+            if (values[i].getClass().getComponentType().isPrimitive()) {
+                Class<?> componentType = values[i].getClass().getComponentType();
+
+                if (componentType == Boolean.TYPE) {
+                    buffer.writeInt(((boolean[]) values[i]).length);
+                    ((BooleanTypeEncoder) typeEncoder).writeRawArray(buffer, state, (boolean[]) values[i]);
+                } else if (componentType == Byte.TYPE) {
+                    buffer.writeInt(((byte[]) values[i]).length);
+                    ((ByteTypeEncoder) typeEncoder).writeRawArray(buffer, state, (byte[]) values[i]);
+                } else if (componentType == Short.TYPE) {
+                    buffer.writeInt(((short[]) values[i]).length);
+                    ((ShortTypeEncoder) typeEncoder).writeRawArray(buffer, state, (short[]) values[i]);
+                } else if (componentType == Integer.TYPE) {
+                    buffer.writeInt(((int[]) values[i]).length);
+                    ((IntegerTypeEncoder) typeEncoder).writeRawArray(buffer, state, (int[]) values[i]);
+                } else if (componentType == Long.TYPE) {
+                    buffer.writeInt(((long[]) values[i]).length);
+                    ((LongTypeEncoder) typeEncoder).writeRawArray(buffer, state, (long[]) values[i]);
+                } else if (componentType == Float.TYPE) {
+                    buffer.writeInt(((Object[]) values[i]).length);
+                    ((FloatTypeEncoder) typeEncoder).writeRawArray(buffer, state, (float[]) values[i]);
+                } else if (componentType == Double.TYPE) {
+                    buffer.writeInt(((double[]) values[i]).length);
+                    ((DoubleTypeEncoder) typeEncoder).writeRawArray(buffer, state, (double[]) values[i]);
+                } else if (componentType == Character.TYPE) {
+                    buffer.writeInt(((float[]) values[i]).length);
+                    ((CharacterTypeEncoder) typeEncoder).writeRawArray(buffer, state, (char[]) values[i]);
+                } else {
+                    throw new IllegalArgumentException(
+                        "Cannot write arrays of type " + componentType.getName());
+                }
+            } else {
+                buffer.writeInt(((Object[]) values[i]).length);
+                typeEncoder.writeRawArray(buffer, state, (Object[]) values[i]);
+            }
+
+            // Move back and write the size
+            long writeSize = buffer.getWriteIndex() - startIndex - Integer.BYTES;
+
+            if (writeSize > Integer.MAX_VALUE) {
+                throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+            }
+
+            buffer.setInt(startIndex, (int) writeSize);
+        }
+    }
+
+    //----- Write methods for primitive arrays -------------------------------//
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, boolean[] value) {
+        final BooleanTypeEncoder typeEncoder = (BooleanTypeEncoder) state.getEncoder().getTypeEncoder(Boolean.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, byte[] value) {
+        final ByteTypeEncoder typeEncoder = (ByteTypeEncoder) state.getEncoder().getTypeEncoder(Byte.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, short[] value) {
+        final ShortTypeEncoder typeEncoder = (ShortTypeEncoder) state.getEncoder().getTypeEncoder(Short.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, int[] value) {
+        final IntegerTypeEncoder typeEncoder = (IntegerTypeEncoder) state.getEncoder().getTypeEncoder(Integer.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, long[] value) {
+        final LongTypeEncoder typeEncoder = (LongTypeEncoder) state.getEncoder().getTypeEncoder(Long.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, float[] value) {
+        final FloatTypeEncoder typeEncoder = (FloatTypeEncoder) state.getEncoder().getTypeEncoder(Float.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, double[] value) {
+        final DoubleTypeEncoder typeEncoder = (DoubleTypeEncoder) state.getEncoder().getTypeEncoder(Double.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, char[] value) {
+        final CharacterTypeEncoder typeEncoder = (CharacterTypeEncoder) state.getEncoder().getTypeEncoder(Character.class);
+        typeEncoder.writeArray(buffer, state, value);
+    }
+
+    //----- Internal support methods -----------------------------------------//
+
+    private TypeEncoder<?> findTypeEncoder(ProtonBuffer buffer, EncoderState state, Object value) {
+        // Scan the array until we either determine an appropriate TypeEncoder or find
+        // that the array is such that encoding it would be invalid.
+
+        if (!value.getClass().isArray()) {
+            throw new IllegalArgumentException("Expected Array type but got: " + value.getClass().getSimpleName());
+        }
+
+        TypeEncoder<?> typeEncoder = null;
+
+        if (value.getClass().getComponentType().isPrimitive()) {
+            Class<?> componentType = value.getClass().getComponentType();
+
+            if (componentType == Boolean.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Boolean.class);
+            } else if (componentType == Byte.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Byte.class);
+            } else if (componentType == Short.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Short.class);
+            } else if (componentType == Integer.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Integer.class);
+            } else if (componentType == Long.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Long.class);
+            } else if (componentType == Float.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Float.class);
+            } else if (componentType == Double.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Double.class);
+            } else if (componentType == Character.TYPE) {
+                typeEncoder = state.getEncoder().getTypeEncoder(Character.class);
+            } else {
+                throw new IllegalArgumentException(
+                    "Cannot write arrays of type " + componentType.getName());
+            }
+        } else {
+            Object[] array = (Object[]) value;
+
+            if (array.length == 0) {
+                if (value.getClass().getComponentType().equals((Object.class))) {
+                    throw new IllegalArgumentException(
+                        "Cannot write a zero sized untyped array.");
+                } else {
+                    typeEncoder = state.getEncoder().getTypeEncoder(value.getClass().getComponentType());
+                }
+            } else {
+                if (array[0].getClass().isArray()) {
+                    typeEncoder = this;
+                } else {
+                    if (array[0].getClass().equals((Object.class))) {
+                        throw new IllegalArgumentException(
+                            "Cannot write a zero sized untyped array.");
+                    }
+
+                    typeEncoder = state.getEncoder().getTypeEncoder(array[0].getClass());
+                }
+            }
+        }
+
+        if (typeEncoder == null) {
+            throw new IllegalArgumentException(
+                "Do not know how to write Objects of class " + value.getClass().getName());
+        }
+
+        return typeEncoder;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/BinaryTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/BinaryTypeEncoder.java
new file mode 100644
index 0000000..f5c30a1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/BinaryTypeEncoder.java
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+
+/**
+ * Encoder of AMQP Binary type values to a byte stream.
+ */
+public final class BinaryTypeEncoder extends AbstractPrimitiveTypeEncoder<Binary> {
+
+    @Override
+    public Class<Binary> getTypeClass() {
+        return Binary.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Binary value) {
+        writeType(buffer, state, value.asProtonBuffer());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, ProtonBuffer value) {
+        if (value.getReadableBytes() > 255) {
+            buffer.writeByte(EncodingCodes.VBIN32);
+            buffer.writeInt(value.getReadableBytes());
+        } else {
+            buffer.writeByte(EncodingCodes.VBIN8);
+            buffer.writeByte((byte) value.getReadableBytes());
+        }
+
+        if (value.hasArray()) {
+            buffer.writeBytes(value.getArray(), value.getArrayOffset() + value.getReadIndex(), value.getReadableBytes());
+        } else {
+            buffer.writeBytes(value, value.getReadIndex(), value.getReadableBytes());
+        }
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, byte[] value) {
+        if (value.length > 255) {
+            buffer.writeByte(EncodingCodes.VBIN32);
+            buffer.writeInt(value.length);
+            buffer.writeBytes(value, 0, value.length);
+        } else {
+            buffer.writeByte(EncodingCodes.VBIN8);
+            buffer.writeByte((byte) value.length);
+            buffer.writeBytes(value, 0, value.length);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.VBIN32);
+        for (Object value : values) {
+            Binary binary = (Binary) value;
+            ProtonBuffer binaryBuffer = binary.asProtonBuffer();
+
+            buffer.writeInt(binaryBuffer.getReadableBytes());
+            binaryBuffer.markReadIndex();
+            try {
+                buffer.writeBytes(binaryBuffer);
+            } finally {
+                binaryBuffer.resetReadIndex();
+            }
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/BooleanTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/BooleanTypeEncoder.java
new file mode 100644
index 0000000..25d522a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/BooleanTypeEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Boolean True types to a byte stream.
+ */
+public final class BooleanTypeEncoder extends AbstractPrimitiveTypeEncoder<Boolean> {
+
+    @Override
+    public Class<Boolean> getTypeClass() {
+        return Boolean.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Boolean value) {
+        buffer.writeByte(value == Boolean.TRUE ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, boolean value) {
+        buffer.writeByte(value == true ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the array elements after writing the array length
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        for (Object bool : values) {
+            buffer.writeByte((Boolean) bool ? 1 : 0);
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, boolean[] values) {
+        // Write the array elements after writing the array length
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        for (boolean bool : values) {
+            buffer.writeByte(bool ? 1 : 0);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, boolean[] values) {
+        if (values.length < 254) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, boolean[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, boolean[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ByteTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ByteTypeEncoder.java
new file mode 100644
index 0000000..602f741
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ByteTypeEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP byte type value to a byte stream.
+ */
+public final class ByteTypeEncoder extends AbstractPrimitiveTypeEncoder<Byte> {
+
+    @Override
+    public Class<Byte> getTypeClass() {
+        return Byte.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Byte value) {
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte(value.byteValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, byte value) {
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte(value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.BYTE);
+        for (Object byteVal : values) {
+            buffer.writeByte(((Byte) byteVal).byteValue());
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, byte[] values) {
+        buffer.writeByte(EncodingCodes.BYTE);
+        for (byte byteVal : values) {
+            buffer.writeByte(byteVal);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, byte[] values) {
+        if (values.length < 254) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, byte[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, byte[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/CharacterTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/CharacterTypeEncoder.java
new file mode 100644
index 0000000..2e3f195
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/CharacterTypeEncoder.java
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Character type values to a byte stream.
+ */
+public final class CharacterTypeEncoder extends AbstractPrimitiveTypeEncoder<Character> {
+
+    @Override
+    public Class<Character> getTypeClass() {
+        return Character.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Character value) {
+        buffer.writeByte(EncodingCodes.CHAR);
+        buffer.writeInt(value.charValue() & 0xffff);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.CHAR);
+        for (Object charValue : values) {
+            buffer.writeInt(((Character) charValue).charValue() & 0xffff);
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, char[] values) {
+        buffer.writeByte(EncodingCodes.CHAR);
+        for (char charValue : values) {
+            buffer.writeInt(charValue & 0xffff);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, char[] values) {
+        if (values.length < 63) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, char[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, char[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal128TypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal128TypeEncoder.java
new file mode 100644
index 0000000..f06c0be
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal128TypeEncoder.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.Decimal128;
+
+/**
+ * Encoder of AMQP Decimal128 type values to a byte stream
+ */
+public final class Decimal128TypeEncoder extends AbstractPrimitiveTypeEncoder<Decimal128> {
+
+    @Override
+    public Class<Decimal128> getTypeClass() {
+        return Decimal128.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Decimal128 value) {
+        buffer.writeByte(EncodingCodes.DECIMAL128);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DECIMAL128);
+        for (Object value : values) {
+            Decimal128 decimal128 = (Decimal128) value;
+            buffer.writeLong(decimal128.getMostSignificantBits());
+            buffer.writeLong(decimal128.getLeastSignificantBits());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal32TypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal32TypeEncoder.java
new file mode 100644
index 0000000..3f86a5c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal32TypeEncoder.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.Decimal32;
+
+/**
+ * Encoder of AMQP Decimal32 type values to a byte stream
+ */
+public final class Decimal32TypeEncoder extends AbstractPrimitiveTypeEncoder<Decimal32> {
+
+    @Override
+    public Class<Decimal32> getTypeClass() {
+        return Decimal32.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Decimal32 value) {
+        buffer.writeByte(EncodingCodes.DECIMAL32);
+        buffer.writeInt(value.getBits());
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DECIMAL32);
+        for (Object value : values) {
+            buffer.writeInt(((Decimal32) value).getBits());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal64TypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal64TypeEncoder.java
new file mode 100644
index 0000000..9b7dcb5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/Decimal64TypeEncoder.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.Decimal64;
+
+/**
+ * Encoder of AMQP Decimal64 type values to a byte stream
+ */
+public final class Decimal64TypeEncoder extends AbstractPrimitiveTypeEncoder<Decimal64> {
+
+    @Override
+    public Class<Decimal64> getTypeClass() {
+        return Decimal64.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Decimal64 value) {
+        buffer.writeByte(EncodingCodes.DECIMAL64);
+        buffer.writeLong(value.getBits());
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DECIMAL64);
+        for (Object value : values) {
+            buffer.writeLong(((Decimal64) value).getBits());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/DoubleTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/DoubleTypeEncoder.java
new file mode 100644
index 0000000..a2fcfd6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/DoubleTypeEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Double type values to a byte stream.
+ */
+public final class DoubleTypeEncoder extends AbstractPrimitiveTypeEncoder<Double> {
+
+    @Override
+    public Class<Double> getTypeClass() {
+        return Double.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Double value) {
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        buffer.writeDouble(value.doubleValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, double value) {
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        buffer.writeDouble(value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        for (Object value : values) {
+            buffer.writeDouble(((Double) value).doubleValue());
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, double[] values) {
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        for (double value : values) {
+            buffer.writeDouble(value);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, double[] values) {
+        if (values.length < 31) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, double[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, double[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/FloatTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/FloatTypeEncoder.java
new file mode 100644
index 0000000..a023a9d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/FloatTypeEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Float type values to a byte stream.
+ */
+public final class FloatTypeEncoder extends AbstractPrimitiveTypeEncoder<Float> {
+
+    @Override
+    public Class<Float> getTypeClass() {
+        return Float.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Float value) {
+        buffer.writeByte(EncodingCodes.FLOAT);
+        buffer.writeFloat(value.floatValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, float value) {
+        buffer.writeByte(EncodingCodes.FLOAT);
+        buffer.writeFloat(value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.FLOAT);
+        for (Object value : values) {
+            buffer.writeFloat(((Float) value).floatValue());
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, float[] values) {
+        buffer.writeByte(EncodingCodes.FLOAT);
+        for (float value : values) {
+            buffer.writeFloat(value);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, float[] values) {
+        if (values.length < 63) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, float[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, float[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/IntegerTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/IntegerTypeEncoder.java
new file mode 100644
index 0000000..ceb9ec2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/IntegerTypeEncoder.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Integer type values to a byte stream.
+ */
+public final class IntegerTypeEncoder extends AbstractPrimitiveTypeEncoder<Integer> {
+
+    @Override
+    public Class<Integer> getTypeClass() {
+        return Integer.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Integer value) {
+        writeType(buffer, state, value.intValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, int value) {
+        if (value >= -128 && value <= 127) {
+            buffer.writeByte(EncodingCodes.SMALLINT);
+            buffer.writeByte((byte) value);
+        } else {
+            buffer.writeByte(EncodingCodes.INT);
+            buffer.writeInt(value);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.INT);
+        for (Object value : values) {
+            buffer.writeInt(((Integer) value).intValue());
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, int[] values) {
+        buffer.writeByte(EncodingCodes.INT);
+        for (int value : values) {
+            buffer.writeInt(value);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, int[] values) {
+        if (values.length < 63) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, int[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, int[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ListTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ListTypeEncoder.java
new file mode 100644
index 0000000..9800df2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ListTypeEncoder.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP List type values to a byte stream.
+ */
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public final class ListTypeEncoder extends AbstractPrimitiveTypeEncoder<List> {
+
+    @Override
+    public Class<List> getTypeClass() {
+        return List.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, List value) {
+        if (value.isEmpty()) {
+            buffer.writeByte(EncodingCodes.LIST0);
+        } else {
+            buffer.writeByte(EncodingCodes.LIST32);
+            writeValue(buffer, state, value);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.LIST32);
+        for (Object value : values) {
+            writeValue(buffer, state, (List) value);
+        }
+    }
+
+    private void writeValue(ProtonBuffer buffer, EncoderState state, List value) {
+        int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size
+        buffer.writeInt(0);
+
+        // Write the count of list elements.
+        buffer.writeInt(value.size());
+
+        TypeEncoder encoder = null;
+
+        // Write the list elements and then compute total size written, try not to lookup
+        // encoders when the types in the list all match.
+        for (int i = 0; i < value.size(); ++i) {
+            Object entry = value.get(i);
+
+            if (encoder == null || !encoder.getTypeClass().equals(entry.getClass())) {
+                encoder = state.getEncoder().getTypeEncoder(entry);
+            }
+
+            if (encoder == null) {
+                throw new EncodeException("Cannot find encoder for type " + entry);
+            }
+
+            encoder.writeType(buffer, state, entry);
+        }
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        buffer.setInt(startIndex, endIndex - startIndex - 4);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/LongTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/LongTypeEncoder.java
new file mode 100644
index 0000000..c4310c4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/LongTypeEncoder.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Integer type values to a byte stream.
+ */
+public final class LongTypeEncoder extends AbstractPrimitiveTypeEncoder<Long> {
+
+    @Override
+    public Class<Long> getTypeClass() {
+        return Long.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Long value) {
+        writeType(buffer, state, value.longValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, long value) {
+        if (value >= -128 && value <= 127) {
+            buffer.writeByte(EncodingCodes.SMALLLONG);
+            buffer.writeByte((byte) value);
+        } else {
+            buffer.writeByte(EncodingCodes.LONG);
+            buffer.writeLong(value);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.LONG);
+        for (Object value : values) {
+            buffer.writeLong(((Long) value).longValue());
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, long[] values) {
+        buffer.writeByte(EncodingCodes.LONG);
+        for (long value : values) {
+            buffer.writeLong(value);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, long[] values) {
+        if (values.length < 31) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, long[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, long[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/MapTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/MapTypeEncoder.java
new file mode 100644
index 0000000..0f34276
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/MapTypeEncoder.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Map type values to a byte stream.
+ */
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public final class MapTypeEncoder extends AbstractPrimitiveTypeEncoder<Map> {
+
+    @Override
+    public Class<Map> getTypeClass() {
+        return Map.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Map value) {
+        buffer.writeByte(EncodingCodes.MAP32);
+        writeValue(buffer, state, value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.MAP32);
+        for (Object value : values) {
+            writeValue(buffer, state, (Map) value);
+        }
+    }
+
+    private void writeValue(ProtonBuffer buffer, EncoderState state, Map value) {
+        int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size
+        buffer.writeInt(0);
+
+        // Record the count of elements which include both key and value in the count.
+        buffer.writeInt(value.size() * 2);
+
+        // Write the list elements and then compute total size written.
+        Set<Map.Entry> entries = value.entrySet();
+        for (Entry entry : entries) {
+            Object entryKey = entry.getKey();
+            Object entryValue = entry.getValue();
+
+            TypeEncoder keyEncoder = state.getEncoder().getTypeEncoder(entryKey);
+            if (keyEncoder == null) {
+                throw new EncodeException("Cannot find encoder for type " + entryKey);
+            }
+
+            keyEncoder.writeType(buffer, state, entryKey);
+
+            TypeEncoder valueEncoder = state.getEncoder().getTypeEncoder(entryValue);
+            if (valueEncoder == null) {
+                throw new EncodeException("Cannot find encoder for type " + entryValue);
+            }
+
+            valueEncoder.writeType(buffer, state, entryValue);
+        }
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        buffer.setInt(startIndex, endIndex - startIndex - 4);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/NullTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/NullTypeEncoder.java
new file mode 100644
index 0000000..dcf444d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/NullTypeEncoder.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Null type values to a byte stream.
+ */
+public final class NullTypeEncoder extends AbstractPrimitiveTypeEncoder<Void> {
+
+    @Override
+    public Class<Void> getTypeClass() {
+        return Void.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Void value) {
+        buffer.writeByte(EncodingCodes.NULL);
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] value) {
+        throw new IllegalArgumentException("Cannot write an array of nulls");
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        throw new IllegalArgumentException("Cannot write an array of nulls");
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ShortTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ShortTypeEncoder.java
new file mode 100644
index 0000000..4198464
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/ShortTypeEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Short type values to a byte stream.
+ */
+public final class ShortTypeEncoder extends AbstractPrimitiveTypeEncoder<Short> {
+
+    @Override
+    public Class<Short> getTypeClass() {
+        return Short.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Short value) {
+        buffer.writeByte(EncodingCodes.SHORT);
+        buffer.writeShort(value.shortValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, short value) {
+        buffer.writeByte(EncodingCodes.SHORT);
+        buffer.writeShort(value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.SHORT);
+        for (Object value : values) {
+            buffer.writeShort(((Short) value).shortValue());
+        }
+    }
+
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, short[] values) {
+        buffer.writeByte(EncodingCodes.SHORT);
+        for (short value : values) {
+            buffer.writeShort(value);
+        }
+    }
+
+    public void writeArray(ProtonBuffer buffer, EncoderState state, short[] values) {
+        if (values.length < 127) {
+            writeAsArray8(buffer, state, values);
+        } else {
+            writeAsArray32(buffer, state, values);
+        }
+    }
+
+    private void writeAsArray8(ProtonBuffer buffer, EncoderState state, short[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY8);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeByte(0);
+        buffer.writeByte(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Byte.BYTES;
+
+        buffer.setByte(startIndex, (byte) writeSize);
+    }
+
+    private void writeAsArray32(ProtonBuffer buffer, EncoderState state, short[] values) {
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        // Write the array elements after writing the array length
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/StringTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/StringTypeEncoder.java
new file mode 100644
index 0000000..0082a45
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/StringTypeEncoder.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP String type values to a byte stream.
+ */
+public final class StringTypeEncoder extends AbstractPrimitiveTypeEncoder<String> {
+
+    @Override
+    public Class<String> getTypeClass() {
+        return String.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, String value) {
+        // We are pessimistic and assume larger strings will encode
+        // at the max 4 bytes per character instead of calculating
+        if (value.length() > 64) {
+            writeString(buffer, state, value);
+        } else {
+            writeSmallString(buffer, state, value);
+        }
+    }
+
+    private static void writeSmallString(ProtonBuffer buffer, EncoderState state, String value) {
+        buffer.writeByte(EncodingCodes.STR8);
+        buffer.writeByte(0);
+
+        int startIndex = buffer.getWriteIndex();
+
+        // Write the full string value
+        state.encodeUTF8(buffer, value);
+
+        // Move back and write the size into the size slot
+        buffer.setByte(startIndex - Byte.BYTES, buffer.getWriteIndex() - startIndex);
+    }
+
+    private static void writeString(ProtonBuffer buffer, EncoderState state, String value) {
+        buffer.writeByte(EncodingCodes.STR32);
+        buffer.writeInt(0);
+
+        int startIndex = buffer.getWriteIndex();
+
+        // Write the full string value
+        state.encodeUTF8(buffer, value);
+
+        // Move back and write the size into the size slot
+        buffer.setInt(startIndex - Integer.BYTES, buffer.getWriteIndex() - startIndex);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.STR32);
+        for (Object value : values) {
+            // Reserve space for the size
+            buffer.writeInt(0);
+
+            int stringStart = buffer.getWriteIndex();
+
+            // Write the full string value
+            state.encodeUTF8(buffer, (CharSequence) value);
+
+            // Move back and write the string size
+            buffer.setInt(stringStart - Integer.BYTES, buffer.getWriteIndex() - stringStart);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/SymbolTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/SymbolTypeEncoder.java
new file mode 100644
index 0000000..5791988
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/SymbolTypeEncoder.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Encoder of AMQP Symbol type values to a byte stream.
+ */
+public final class SymbolTypeEncoder extends AbstractPrimitiveTypeEncoder<Symbol> {
+
+    @Override
+    public Class<Symbol> getTypeClass() {
+        return Symbol.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Symbol value) {
+        int symbolBytes = value.getLength();
+
+        if (symbolBytes <= 255) {
+            buffer.writeByte(EncodingCodes.SYM8);
+            buffer.writeByte(symbolBytes);
+        } else {
+            buffer.writeByte(EncodingCodes.SYM32);
+            buffer.writeInt(symbolBytes);
+        }
+
+        value.writeTo(buffer);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.SYM32);
+        for (Object value : values) {
+            Symbol symbol = (Symbol) value;
+            buffer.writeInt(symbol.getLength());
+            symbol.writeTo(buffer);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/TimestampTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/TimestampTypeEncoder.java
new file mode 100644
index 0000000..cfc900a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/TimestampTypeEncoder.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import java.util.Date;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP Timestamp type values to a byte stream.
+ */
+public final class TimestampTypeEncoder extends AbstractPrimitiveTypeEncoder<Date> {
+
+    @Override
+    public Class<Date> getTypeClass() {
+        return Date.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, Date value) {
+        buffer.writeByte(EncodingCodes.TIMESTAMP);
+        buffer.writeLong(value.getTime());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, long value) {
+        buffer.writeByte(EncodingCodes.TIMESTAMP);
+        buffer.writeLong(value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.LONG);
+        for (Object value : values) {
+            buffer.writeLong(((Date) value).getTime());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UUIDTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UUIDTypeEncoder.java
new file mode 100644
index 0000000..f527c3b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UUIDTypeEncoder.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+
+/**
+ * Encoder of AMQP UUID type value to a byte stream.
+ */
+public final class UUIDTypeEncoder extends AbstractPrimitiveTypeEncoder<UUID> {
+
+    @Override
+    public Class<UUID> getTypeClass() {
+        return UUID.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, UUID value) {
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.UUID);
+        for (Object value : values) {
+            UUID uuid = (UUID) value;
+            buffer.writeLong(uuid.getMostSignificantBits());
+            buffer.writeLong(uuid.getLeastSignificantBits());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedByteTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedByteTypeEncoder.java
new file mode 100644
index 0000000..89abc22
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedByteTypeEncoder.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+
+/**
+ * Encoder of AMQP UnsignedByte type values to a byte stream
+ */
+public final class UnsignedByteTypeEncoder extends AbstractPrimitiveTypeEncoder<UnsignedByte> {
+
+    @Override
+    public Class<UnsignedByte> getTypeClass() {
+        return UnsignedByte.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, UnsignedByte value) {
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(value.byteValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, byte value) {
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.UBYTE);
+        for (Object value : values) {
+            buffer.writeByte(((UnsignedByte) value).byteValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedIntegerTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedIntegerTypeEncoder.java
new file mode 100644
index 0000000..3bc31bb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedIntegerTypeEncoder.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Encoder of AMQP UnsignedShort type values to a byte stream.
+ */
+public final class UnsignedIntegerTypeEncoder extends AbstractPrimitiveTypeEncoder<UnsignedInteger> {
+
+    @Override
+    public Class<UnsignedInteger> getTypeClass() {
+        return UnsignedInteger.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, UnsignedInteger value) {
+        int intValue = value.intValue();
+
+        if (intValue == 0) {
+            buffer.writeByte(EncodingCodes.UINT0);
+        } else if (intValue > 0 && intValue <= 255) {
+            buffer.writeByte(EncodingCodes.SMALLUINT);
+            buffer.writeByte(intValue);
+        } else {
+            buffer.writeByte(EncodingCodes.UINT);
+            buffer.writeInt(intValue);
+        }
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, byte value) {
+        if (value == 0) {
+            buffer.writeByte(EncodingCodes.UINT0);
+        } else {
+            buffer.writeByte(EncodingCodes.SMALLUINT);
+            buffer.writeByte(value);
+        }
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, int value) {
+        if (value == 0) {
+            buffer.writeByte(EncodingCodes.UINT0);
+        } else if (value > 0 && value <= 255) {
+            buffer.writeByte(EncodingCodes.SMALLUINT);
+            buffer.writeByte(value);
+        } else {
+            buffer.writeByte(EncodingCodes.UINT);
+            buffer.writeInt(value);
+        }
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, long value) {
+        if (value < 0L || value >= (1L << 32)) {
+            throw new IllegalArgumentException("Value \"" + value + "\" lies outside the range [" + 0L + "-" + (1L << 32) + ").");
+        }
+
+        int intValue = (int) value;
+
+        if (intValue == 0) {
+            buffer.writeByte(EncodingCodes.UINT0);
+        } else if (intValue > 0 && intValue <= 255) {
+            buffer.writeByte(EncodingCodes.SMALLUINT);
+            buffer.writeByte(intValue);
+        } else {
+            buffer.writeByte(EncodingCodes.UINT);
+            buffer.writeInt(intValue);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.UINT);
+        for (Object value : values) {
+            buffer.writeInt(((UnsignedInteger) value).intValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedLongTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedLongTypeEncoder.java
new file mode 100644
index 0000000..24f758f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedLongTypeEncoder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * Encoder of AMQP UnsignedShort type values to a byte stream.
+ */
+public final class UnsignedLongTypeEncoder extends AbstractPrimitiveTypeEncoder<UnsignedLong> {
+
+    @Override
+    public Class<UnsignedLong> getTypeClass() {
+        return UnsignedLong.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, UnsignedLong value) {
+        writeType(buffer, state, value.longValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, long value) {
+        if (value == 0) {
+            buffer.writeByte(EncodingCodes.ULONG0);
+        } else if (value > 0 && value <= 255) {
+            buffer.writeByte(EncodingCodes.SMALLULONG);
+            buffer.writeByte((int) value);
+        } else {
+            buffer.writeByte(EncodingCodes.ULONG);
+            buffer.writeLong(value);
+        }
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, byte value) {
+        if (value == 0) {
+            buffer.writeByte(EncodingCodes.ULONG0);
+        } else {
+            buffer.writeByte(EncodingCodes.SMALLULONG);
+            buffer.writeByte(value);
+        }
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.ULONG);
+        for (Object value : values) {
+            buffer.writeLong(((UnsignedLong)value).longValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedShortTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedShortTypeEncoder.java
new file mode 100644
index 0000000..050b0e6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/primitives/UnsignedShortTypeEncoder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.primitives;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractPrimitiveTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+/**
+ * Encoder of AMQP UnsignedShort type values to a byte stream.
+ */
+public final class UnsignedShortTypeEncoder extends AbstractPrimitiveTypeEncoder<UnsignedShort> {
+
+    @Override
+    public Class<UnsignedShort> getTypeClass() {
+        return UnsignedShort.class;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, UnsignedShort value) {
+        buffer.writeByte(EncodingCodes.USHORT);
+        buffer.writeShort(value.shortValue());
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, short value) {
+        buffer.writeByte(EncodingCodes.USHORT);
+        buffer.writeShort(value);
+    }
+
+    public void writeType(ProtonBuffer buffer, EncoderState state, int value) {
+        if (value < 0 || value > 65535) {
+            throw new IllegalArgumentException("Value given is out of range: " + value);
+        }
+
+        writeType(buffer, state, (short) value);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.USHORT);
+        for (Object value : values) {
+            buffer.writeShort(((UnsignedShort)value).shortValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslChallengeTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslChallengeTypeEncoder.java
new file mode 100644
index 0000000..ac70faa
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslChallengeTypeEncoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslChallenge;
+
+/**
+ * Encoder of AMQP SaslChallenge type values to a byte stream
+ */
+public final class SaslChallengeTypeEncoder extends AbstractDescribedListTypeEncoder<SaslChallenge> {
+
+    @Override
+    public Class<SaslChallenge> getTypeClass() {
+        return SaslChallenge.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslChallenge.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslChallenge.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeElement(SaslChallenge challenge, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeBinary(buffer, state, challenge.getChallenge());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown SaslChallenge value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(SaslChallenge value) {
+        if (value.getChallenge().getReadableBytes() < 255) {
+            return EncodingCodes.LIST8;
+        } else {
+            return EncodingCodes.LIST32;
+        }
+    }
+
+    @Override
+    public int getElementCount(SaslChallenge challenge) {
+        return 1;
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslInitTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslInitTypeEncoder.java
new file mode 100644
index 0000000..e874cc1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslInitTypeEncoder.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+
+/**
+ * Encoder of AMQP SaslInit type values to a byte stream
+ */
+public final class SaslInitTypeEncoder extends AbstractDescribedListTypeEncoder<SaslInit> {
+
+    @Override
+    public Class<SaslInit> getTypeClass() {
+        return SaslInit.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslInit.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslInit.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeElement(SaslInit init, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeSymbol(buffer, state, init.getMechanism());
+                break;
+            case 1:
+                state.getEncoder().writeBinary(buffer, state, init.getInitialResponse());
+                break;
+            case 2:
+                state.getEncoder().writeString(buffer, state, init.getHostname());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown SaslInit value index: " + index);
+        }
+    }
+
+    @Override
+    public int getElementCount(SaslInit init) {
+        if (init.getHostname() != null) {
+            return 3;
+        } else if (init.getInitialResponse() != null) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslMechanismsTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslMechanismsTypeEncoder.java
new file mode 100644
index 0000000..af8f55e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslMechanismsTypeEncoder.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslMechanisms;
+
+/**
+ * Encoder of AMQP SaslMechanisms type values to a byte stream
+ */
+public final class SaslMechanismsTypeEncoder extends AbstractDescribedListTypeEncoder<SaslMechanisms> {
+
+    @Override
+    public Class<SaslMechanisms> getTypeClass() {
+        return SaslMechanisms.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslMechanisms.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslMechanisms.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeElement(SaslMechanisms mechanisms, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeArray(buffer, state, mechanisms.getSaslServerMechanisms());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown SaslChallenge value index: " + index);
+        }
+    }
+
+    @Override
+    public int getElementCount(SaslMechanisms challenge) {
+        return 1;
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslOutcomeTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslOutcomeTypeEncoder.java
new file mode 100644
index 0000000..3596f2d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslOutcomeTypeEncoder.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslOutcome;
+
+/**
+ * Encoder of AMQP SaslOutcome type values to a byte stream
+ */
+public final class SaslOutcomeTypeEncoder extends AbstractDescribedListTypeEncoder<SaslOutcome> {
+
+    @Override
+    public Class<SaslOutcome> getTypeClass() {
+        return SaslOutcome.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslOutcome.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslOutcome.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeElement(SaslOutcome outcome, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeUnsignedByte(buffer, state, outcome.getCode().getValue());
+                break;
+            case 1:
+                state.getEncoder().writeBinary(buffer, state, outcome.getAdditionalData());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown SaslOutcome value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(SaslOutcome value) {
+        if (value.getAdditionalData() == null || value.getAdditionalData().getReadableBytes() < 253) {
+            return EncodingCodes.LIST8;
+        } else {
+            return EncodingCodes.LIST32;
+        }
+    }
+
+    @Override
+    public int getElementCount(SaslOutcome outcome) {
+        if (outcome.getAdditionalData() != null) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslResponseTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslResponseTypeEncoder.java
new file mode 100644
index 0000000..df89468
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/security/SaslResponseTypeEncoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.security.SaslResponse;
+
+/**
+ * Encoder of AMQP SaslResponse type values to a byte stream
+ */
+public final class SaslResponseTypeEncoder extends AbstractDescribedListTypeEncoder<SaslResponse> {
+
+    @Override
+    public Class<SaslResponse> getTypeClass() {
+        return SaslResponse.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return SaslResponse.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return SaslResponse.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public byte getListEncoding(SaslResponse value) {
+        if (value.getResponse().getReadableBytes() < 255) {
+            return EncodingCodes.LIST8;
+        } else {
+            return EncodingCodes.LIST32;
+        }
+    }
+
+    @Override
+    public void writeElement(SaslResponse response, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeBinary(buffer, state, response.getResponse());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown SaslResponse value index: " + index);
+        }
+    }
+
+    @Override
+    public int getElementCount(SaslResponse challenge) {
+        return 1;
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/CoordinatorTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/CoordinatorTypeEncoder.java
new file mode 100644
index 0000000..88c5048
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/CoordinatorTypeEncoder.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transactions;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+
+/**
+ * Encoder of AMQP Coordinator type values to a byte stream.
+ */
+public final class CoordinatorTypeEncoder extends AbstractDescribedListTypeEncoder<Coordinator> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Coordinator.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Coordinator.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Coordinator> getTypeClass() {
+        return Coordinator.class;
+    }
+
+    @Override
+    public void writeElement(Coordinator coordinator, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeArray(buffer, state, coordinator.getCapabilities());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Coordinator value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Coordinator value) {
+        if (value.getCapabilities() != null) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST0;
+        }
+    }
+
+    @Override
+    public int getElementCount(Coordinator coordinator) {
+        if (coordinator.getCapabilities() != null) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DeclareTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DeclareTypeEncoder.java
new file mode 100644
index 0000000..beeaa81
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DeclareTypeEncoder.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transactions;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+
+/**
+ * Encoder of AMQP Declare type values to a byte stream.
+ */
+public final class DeclareTypeEncoder extends AbstractDescribedListTypeEncoder<Declare> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Declare.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Declare.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Declare> getTypeClass() {
+        return Declare.class;
+    }
+
+    @Override
+    public void writeElement(Declare declare, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeObject(buffer, state, declare.getGlobalId());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Declare value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Declare value) {
+        if (value.getGlobalId() != null) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST0;
+        }
+    }
+
+    @Override
+    public int getElementCount(Declare declare) {
+        if (declare.getGlobalId() != null) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DeclaredTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DeclaredTypeEncoder.java
new file mode 100644
index 0000000..4eebf77
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DeclaredTypeEncoder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transactions;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+
+/**
+ * Encoder of AMQP Declared type values to a byte stream.
+ */
+public final class DeclaredTypeEncoder extends AbstractDescribedListTypeEncoder<Declared> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Declared.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Declared.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Declared> getTypeClass() {
+        return Declared.class;
+    }
+
+    @Override
+    public void writeElement(Declared declared, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeBinary(buffer, state, declared.getTxnId());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Declared value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Declared value) {
+        if (value.getTxnId() != null && value.getTxnId().getLength() > 255) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST8;
+        }
+    }
+
+    @Override
+    public int getElementCount(Declared declared) {
+        return 1;
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DischargeTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DischargeTypeEncoder.java
new file mode 100644
index 0000000..b78efff
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/DischargeTypeEncoder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transactions;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+
+/**
+ * Encoder of AMQP Discharge type values to a byte stream.
+ */
+public final class DischargeTypeEncoder extends AbstractDescribedListTypeEncoder<Discharge> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Discharge.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Discharge.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Discharge> getTypeClass() {
+        return Discharge.class;
+    }
+
+    @Override
+    public void writeElement(Discharge discharge, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeBinary(buffer, state, discharge.getTxnId());
+                break;
+            case 1:
+                buffer.writeByte(discharge.getFail() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Discharge value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Discharge value) {
+        if (value.getTxnId() != null && value.getTxnId().getLength() > 240) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST8;
+        }
+    }
+
+    @Override
+    public int getElementCount(Discharge discharge) {
+        return 2;
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/TransactionStateTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/TransactionStateTypeEncoder.java
new file mode 100644
index 0000000..7459459
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transactions/TransactionStateTypeEncoder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transactions;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+
+/**
+ * Encoder of AMQP TransactionState type values to a byte stream.
+ */
+public final class TransactionStateTypeEncoder extends AbstractDescribedListTypeEncoder<TransactionalState> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return TransactionalState.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return TransactionalState.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<TransactionalState> getTypeClass() {
+        return TransactionalState.class;
+    }
+
+    @Override
+    public void writeElement(TransactionalState txState, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeBinary(buffer, state, txState.getTxnId());
+                break;
+            case 1:
+                state.getEncoder().writeObject(buffer, state, txState.getOutcome());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown TransactionalState value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(TransactionalState value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(TransactionalState txState) {
+        if (txState.getOutcome() != null) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/AttachTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/AttachTypeEncoder.java
new file mode 100644
index 0000000..0320c46
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/AttachTypeEncoder.java
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Attach;
+
+/**
+ * Encoder of AMQP Attach type values to a byte stream.
+ */
+public final class AttachTypeEncoder extends AbstractDescribedListTypeEncoder<Attach> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Attach.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Attach.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Attach> getTypeClass() {
+        return Attach.class;
+    }
+
+    @Override
+    public void writeElement(Attach attach, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (attach.hasName()) {
+                    state.getEncoder().writeString(buffer, state, attach.getName());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                if (attach.hasHandle()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, attach.getHandle());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 2:
+                if (attach.hasRole()) {
+                    buffer.writeByte(attach.getRole().getValue() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 3:
+                if (attach.hasSenderSettleMode()) {
+                    state.getEncoder().writeUnsignedByte(buffer, state, attach.getSenderSettleMode().byteValue());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 4:
+                if (attach.hasReceiverSettleMode()) {
+                    state.getEncoder().writeUnsignedByte(buffer, state, attach.getReceiverSettleMode().byteValue());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 5:
+                if (attach.hasSource()) {
+                    state.getEncoder().writeObject(buffer, state, attach.getSource());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 6:
+                if (attach.hasTargetOrCoordinator()) {
+                    state.getEncoder().writeObject(buffer, state, attach.getTarget());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 7:
+                if (attach.hasUnsettled()) {
+                    state.getEncoder().writeMap(buffer, state, attach.getUnsettled());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 8:
+                if (attach.hasIncompleteUnsettled()) {
+                    buffer.writeByte(attach.getIncompleteUnsettled() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 9:
+                if (attach.hasInitialDeliveryCount()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, attach.getInitialDeliveryCount());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 10:
+                if (attach.hasMaxMessageSize()) {
+                    state.getEncoder().writeUnsignedLong(buffer, state, attach.getMaxMessageSize());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 11:
+                if (attach.hasOfferedCapabilites()) {
+                    state.getEncoder().writeArray(buffer, state, attach.getOfferedCapabilities());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 12:
+                if (attach.hasDesiredCapabilites()) {
+                    state.getEncoder().writeArray(buffer, state, attach.getDesiredCapabilities());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 13:
+                if (attach.hasProperties()) {
+                    state.getEncoder().writeMap(buffer, state, attach.getProperties());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Attach value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Attach value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Attach attach) {
+        return attach.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 3;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/BeginTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/BeginTypeEncoder.java
new file mode 100644
index 0000000..452b3f1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/BeginTypeEncoder.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Begin;
+
+/**
+ * Encoder of AMQP Begin type values to a byte stream.
+ */
+public final class BeginTypeEncoder extends AbstractDescribedListTypeEncoder<Begin> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Begin.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Begin.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Begin> getTypeClass() {
+        return Begin.class;
+    }
+
+    @Override
+    public void writeElement(Begin begin, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (begin.hasRemoteChannel()) {
+                    state.getEncoder().writeUnsignedShort(buffer, state, begin.getRemoteChannel());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                if (begin.hasNextOutgoingId()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, begin.getNextOutgoingId());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 2:
+                if (begin.hasIncomingWindow()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, begin.getIncomingWindow());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 3:
+                if (begin.hasOutgoingWindow()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, begin.getOutgoingWindow());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 4:
+                if (begin.hasHandleMax()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, begin.getHandleMax());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 5:
+                if (begin.hasOfferedCapabilites()) {
+                    state.getEncoder().writeArray(buffer, state, begin.getOfferedCapabilities());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 6:
+                if (begin.hasDesiredCapabilites()) {
+                    state.getEncoder().writeArray(buffer, state, begin.getDesiredCapabilities());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 7:
+                if (begin.hasProperties()) {
+                    state.getEncoder().writeMap(buffer, state, begin.getProperties());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Begin value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Begin value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Begin begin) {
+        return begin.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 4;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/CloseTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/CloseTypeEncoder.java
new file mode 100644
index 0000000..830501a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/CloseTypeEncoder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Close;
+
+/**
+ * Encoder of AMQP Close type values to a byte stream/
+ */
+public final class CloseTypeEncoder extends AbstractDescribedListTypeEncoder<Close> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Close.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Close.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Close> getTypeClass() {
+        return Close.class;
+    }
+
+    @Override
+    public void writeElement(Close close, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeObject(buffer, state, close.getError());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Close value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Close value) {
+        return value.getError() == null ? EncodingCodes.LIST0 : EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Close value) {
+        return value.getError() == null ? 0 : 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/DetachTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/DetachTypeEncoder.java
new file mode 100644
index 0000000..348a6b3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/DetachTypeEncoder.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Detach;
+
+/**
+ * Encoder of AMQP Detach type values to a byte stream.
+ */
+public final class DetachTypeEncoder extends AbstractDescribedListTypeEncoder<Detach> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Detach.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Detach.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Detach> getTypeClass() {
+        return Detach.class;
+    }
+
+    @Override
+    public void writeElement(Detach detach, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (detach.hasHandle()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, detach.getHandle());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                buffer.writeByte(detach.getClosed() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                break;
+            case 2:
+                state.getEncoder().writeObject(buffer, state, detach.getError());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Detach value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Detach value) {
+        return value.getError() == null ? EncodingCodes.LIST8 : EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Detach detach) {
+        return detach.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/DispositionTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/DispositionTypeEncoder.java
new file mode 100644
index 0000000..73a1fb5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/DispositionTypeEncoder.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+
+/**
+ * Encoder of AMQP Disposition type values to a byte stream
+ */
+public final class DispositionTypeEncoder extends AbstractDescribedListTypeEncoder<Disposition> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Disposition.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Disposition.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Disposition> getTypeClass() {
+        return Disposition.class;
+    }
+
+    @Override
+    public void writeElement(Disposition disposition, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (disposition.hasRole()) {
+                    buffer.writeByte(disposition.getRole().getValue() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                if (disposition.hasFirst()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, disposition.getFirst());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 2:
+                if (disposition.hasLast()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, disposition.getLast());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 3:
+                if (disposition.hasSettled()) {
+                    buffer.writeByte(disposition.getSettled() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 4:
+                if (disposition.hasState()) {
+                    if (disposition.getState() == Accepted.getInstance()) {
+                        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+                        buffer.writeByte(EncodingCodes.SMALLULONG);
+                        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+                        buffer.writeByte(EncodingCodes.LIST0);
+                    } else {
+                        state.getEncoder().writeObject(buffer, state, disposition.getState());
+                    }
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+
+                break;
+            case 5:
+                if (disposition.hasBatchable()) {
+                    buffer.writeByte(disposition.getBatchable() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Disposition value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Disposition value) {
+        if (value.getState() == null) {
+            return EncodingCodes.LIST8;
+        } else if (value.getState() == Accepted.getInstance() || value.getState() == Released.getInstance()) {
+            return EncodingCodes.LIST8;
+        } else {
+            return EncodingCodes.LIST32;
+        }
+    }
+
+    @Override
+    public int getElementCount(Disposition disposition) {
+        return disposition.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 2;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/EndTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/EndTypeEncoder.java
new file mode 100644
index 0000000..169ad30
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/EndTypeEncoder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.End;
+
+/**
+ * Encoder of AMQP End type values to a byte stream.
+ */
+public final class EndTypeEncoder extends AbstractDescribedListTypeEncoder<End> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return End.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return End.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<End> getTypeClass() {
+        return End.class;
+    }
+
+    @Override
+    public void writeElement(End end, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeObject(buffer, state, end.getError());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown End value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(End value) {
+        return value.getError() == null ? EncodingCodes.LIST0 : EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(End end) {
+        return end.getError() == null ? 0 : 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/ErrorConditionTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/ErrorConditionTypeEncoder.java
new file mode 100644
index 0000000..373ae94
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/ErrorConditionTypeEncoder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Encoder of AMQP ErrorCondition type values to a byte stream
+ */
+public final class ErrorConditionTypeEncoder extends AbstractDescribedListTypeEncoder<ErrorCondition> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return ErrorCondition.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return ErrorCondition.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<ErrorCondition> getTypeClass() {
+        return ErrorCondition.class;
+    }
+
+    @Override
+    public void writeElement(ErrorCondition error, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                state.getEncoder().writeSymbol(buffer, state, error.getCondition());
+                break;
+            case 1:
+                state.getEncoder().writeString(buffer, state, error.getDescription());
+                break;
+            case 2:
+                state.getEncoder().writeMap(buffer, state, error.getInfo());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown ErrorCondition value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(ErrorCondition value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(ErrorCondition error) {
+        if (error.getInfo() != null) {
+            return 3;
+        } else if (error.getDescription() != null) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/FlowTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/FlowTypeEncoder.java
new file mode 100644
index 0000000..3b5ff65
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/FlowTypeEncoder.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Flow;
+
+/**
+ * Encoder of AMQP Flow type values to a byte stream.
+ */
+public final class FlowTypeEncoder extends AbstractDescribedListTypeEncoder<Flow> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Flow.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Flow.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Flow> getTypeClass() {
+        return Flow.class;
+    }
+
+    @Override
+    public void writeElement(Flow flow, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (flow.hasNextIncomingId()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getNextIncomingId());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                if (flow.hasIncomingWindow()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getIncomingWindow());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 2:
+                if (flow.hasNextOutgoingId()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getNextOutgoingId());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 3:
+                if (flow.hasOutgoingWindow()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getOutgoingWindow());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 4:
+                if (flow.hasHandle()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getHandle());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 5:
+                if (flow.hasDeliveryCount()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getDeliveryCount());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 6:
+                if (flow.hasLinkCredit()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getLinkCredit());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 7:
+                if (flow.hasAvailable()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, flow.getAvailable());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 8:
+                if (flow.hasDrain()) {
+                    buffer.writeByte(flow.getDrain() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 9:
+                if (flow.hasEcho()) {
+                    buffer.writeByte(flow.getEcho() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 10:
+                state.getEncoder().writeMap(buffer, state, flow.getProperties());
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Flow value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Flow value) {
+        if (value.getProperties() == null) {
+            return EncodingCodes.LIST8;
+        } else {
+            return EncodingCodes.LIST32;
+        }
+    }
+
+    @Override
+    public int getElementCount(Flow flow) {
+        return flow.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 4;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/OpenTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/OpenTypeEncoder.java
new file mode 100644
index 0000000..1d88366
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/OpenTypeEncoder.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Open;
+
+/**
+ * Encoder of AMQP Open type values to a byte stream.
+ */
+public final class OpenTypeEncoder extends AbstractDescribedListTypeEncoder<Open> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Open.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Open.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Open> getTypeClass() {
+        return Open.class;
+    }
+
+    @Override
+    public void writeElement(Open open, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (open.hasContainerId()) {
+                    state.getEncoder().writeString(buffer, state, open.getContainerId());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 1:
+                if (open.hasHostname()) {
+                    state.getEncoder().writeString(buffer, state, open.getHostname());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 2:
+                if (open.hasMaxFrameSize()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, open.getMaxFrameSize());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 3:
+                if (open.hasChannelMax()) {
+                    state.getEncoder().writeUnsignedShort(buffer, state, open.getChannelMax());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 4:
+                if (open.hasIdleTimeout()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, open.getIdleTimeout());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 5:
+                if (open.hasOutgoingLocales()) {
+                    state.getEncoder().writeArray(buffer, state, open.getOutgoingLocales());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 6:
+                if (open.hasIncomingLocales()) {
+                    state.getEncoder().writeArray(buffer, state, open.getIncomingLocales());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 7:
+                if (open.hasOfferedCapabilites()) {
+                    state.getEncoder().writeArray(buffer, state, open.getOfferedCapabilities());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 8:
+                if (open.hasDesiredCapabilites()) {
+                    state.getEncoder().writeArray(buffer, state, open.getDesiredCapabilities());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            case 9:
+                if (open.hasProperties()) {
+                    state.getEncoder().writeMap(buffer, state, open.getProperties());
+                } else {
+                    state.getEncoder().writeNull(buffer, state);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Open value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Open value) {
+        return EncodingCodes.LIST32;
+    }
+
+    @Override
+    public int getElementCount(Open open) {
+        return open.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/TransferTypeEncoder.java b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/TransferTypeEncoder.java
new file mode 100644
index 0000000..8c50fad
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/codec/encoders/transport/TransferTypeEncoder.java
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedListTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Encoder of AMQP Transfer type values to a byte stream.
+ */
+public final class TransferTypeEncoder extends AbstractDescribedListTypeEncoder<Transfer> {
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return Transfer.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return Transfer.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public Class<Transfer> getTypeClass() {
+        return Transfer.class;
+    }
+
+    @Override
+    public void writeElement(Transfer transfer, int index, ProtonBuffer buffer, EncoderState state) {
+        switch (index) {
+            case 0:
+                if (transfer.hasHandle()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, transfer.getHandle());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 1:
+                if (transfer.hasDeliveryId()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, transfer.getDeliveryId());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 2:
+                if (transfer.hasDeliveryTag()) {
+                    state.getEncoder().writeDeliveryTag(buffer, state, transfer.getDeliveryTag());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 3:
+                if (transfer.hasMessageFormat()) {
+                    state.getEncoder().writeUnsignedInteger(buffer, state, transfer.getMessageFormat());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 4:
+                if (transfer.hasSettled()) {
+                    buffer.writeByte(transfer.getSettled() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 5:
+                if (transfer.hasMore()) {
+                    buffer.writeByte(transfer.getMore() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 6:
+                if (transfer.hasRcvSettleMode()) {
+                    state.getEncoder().writeUnsignedByte(buffer, state, transfer.getRcvSettleMode().byteValue());
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 7:
+                state.getEncoder().writeObject(buffer, state, transfer.getState());
+                break;
+            case 8:
+                if (transfer.hasResume()) {
+                    buffer.writeByte(transfer.getResume() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 9:
+                if (transfer.hasAborted()) {
+                    buffer.writeByte(transfer.getAborted() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            case 10:
+                if (transfer.hasBatchable()) {
+                    buffer.writeByte(transfer.getBatchable() ? EncodingCodes.BOOLEAN_TRUE : EncodingCodes.BOOLEAN_FALSE);
+                } else {
+                    buffer.writeByte(EncodingCodes.NULL);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Transfer value index: " + index);
+        }
+    }
+
+    @Override
+    public byte getListEncoding(Transfer value) {
+        if (value.getState() != null) {
+            return EncodingCodes.LIST32;
+        } else if (value.getDeliveryTag() != null && value.getDeliveryTag().tagLength() > 200) {
+            return EncodingCodes.LIST32;
+        } else {
+            return EncodingCodes.LIST8;
+        }
+    }
+
+    @Override
+    public int getElementCount(Transfer transfer) {
+        return transfer.getElementCount();
+    }
+
+    @Override
+    public int getMinElementCount() {
+        return 1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/AMQPPerformativeEnvelopePool.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/AMQPPerformativeEnvelopePool.java
new file mode 100644
index 0000000..585d973
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/AMQPPerformativeEnvelopePool.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.function.Function;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.util.RingQueue;
+import org.apache.qpid.protonj2.types.transport.Performative;
+
+/**
+ * Pool of {@link PerformativeEnvelope} instances used to reduce allocations on incoming performatives.
+ *
+ * @param <E> The type of Protocol Performative to pool incoming or outgoing.
+ */
+public class AMQPPerformativeEnvelopePool<E extends PerformativeEnvelope<Performative>> {
+
+    public static final int DEFAULT_MAX_POOL_SIZE = 10;
+
+    private int maxPoolSize = DEFAULT_MAX_POOL_SIZE;
+
+    private final RingQueue<E> pool;
+    private final Function<AMQPPerformativeEnvelopePool<E>, E> envelopeBuilder;
+
+    public AMQPPerformativeEnvelopePool(Function<AMQPPerformativeEnvelopePool<E>, E> envelopeBuilder) {
+        this(envelopeBuilder, AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE);
+    }
+
+    public AMQPPerformativeEnvelopePool(Function<AMQPPerformativeEnvelopePool<E>, E> envelopeBuilder, int maxPoolSize) {
+        this.pool = new RingQueue<>(getMaxPoolSize());
+        this.maxPoolSize = maxPoolSize;
+        this.envelopeBuilder = envelopeBuilder;
+    }
+
+    public final int getMaxPoolSize() {
+        return maxPoolSize;
+    }
+
+    @SuppressWarnings("unchecked")
+    public E take(Performative body, int channel, ProtonBuffer payload) {
+        return (E) pool.poll(this::supplyPooledResource).initialize(body, channel, payload);
+    }
+
+    void release(E pooledEnvelope) {
+        pool.offer(pooledEnvelope);
+    }
+
+    private E supplyPooledResource() {
+        return envelopeBuilder.apply(this);
+    }
+
+    /**
+     * @param maxPoolSize
+     *      The maximum number of protocol envelopes to store in the pool.
+     *
+     * @return a new {@link AMQPPerformativeEnvelopePool} that pools incoming AMQP envelopes
+     */
+    public static AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> incomingEnvelopePool(int maxPoolSize) {
+        return new AMQPPerformativeEnvelopePool<>((pool) -> new IncomingAMQPEnvelope(pool), maxPoolSize);
+    }
+
+    /**
+     * @return a new {@link AMQPPerformativeEnvelopePool} that pools incoming AMQP envelopes
+     */
+    public static AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> incomingEnvelopePool() {
+        return new AMQPPerformativeEnvelopePool<>((pool) -> new IncomingAMQPEnvelope(pool));
+    }
+
+    /**
+     * @param maxPoolSize
+     *      The maximum number of protocol envelopes to store in the pool.
+     *
+     * @return a new {@link AMQPPerformativeEnvelopePool} that pools outgoing AMQP envelopes
+     */
+    public static AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> outgoingEnvelopePool(int maxPoolSize) {
+        return new AMQPPerformativeEnvelopePool<>((pool) -> new OutgoingAMQPEnvelope(pool), maxPoolSize);
+    }
+
+    /**
+     * @return a new {@link AMQPPerformativeEnvelopePool} that pools outgoing AMQP envelopes
+     */
+    public static AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> outgoingEnvelopePool() {
+        return new AMQPPerformativeEnvelopePool<>((pool) -> new OutgoingAMQPEnvelope(pool));
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Attachments.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Attachments.java
new file mode 100644
index 0000000..3e0e9c9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Attachments.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * Attachments API used to associate specific data with AMQP Resources
+ */
+public interface Attachments {
+
+    /**
+     * Gets the user attached value that is associated with the given key, or null
+     * if no data is mapped to the key.
+     *
+     * @param <T> The type to cast the attached mapped value to if one is set.
+     *
+     * @param key
+     *      The key to use to lookup the mapped data.
+     *
+     * @return the object associated with the given key in this {@link Attachments} instance.
+     */
+    <T> T get(String key);
+
+    /**
+     * Gets the user set {@link Attachments} value that is associated with the given key, or null
+     * if no data is mapped to the key.
+     *
+     * @param <T> The type to cast the attached mapped value to if one is set.
+     *
+     * @param key
+     *      The key to use to lookup the mapped data.
+     * @param typeClass
+     *      The Class that will be used when casting the returned mapped object.
+     *
+     * @return the object associated with the given key in this {@link Attachments} instance.
+     */
+    <T> T get(String key, Class<T> typeClass);
+
+    /**
+     * Maps a given object to the given key in this {@link Attachments} instance.
+     *
+     * @param <T> The type of the value being set
+     *
+     * @param key
+     *      The key to assign the value to
+     * @param value
+     *      The value to map to the given key.
+     *
+     * @return this {@link Attachments} instance.
+     */
+    <T> Attachments set(String key, T value);
+
+    /**
+     * Checks if the given key has a value mapped to it in this {@link Attachments} instance.
+     *
+     * @param key
+     *      The key to search for a mapping to in this {@link Attachments} instance.
+     *
+     * @return true if there is a value mapped to the given key in this {@link Attachments} instance.
+     */
+    boolean containsKey(String key);
+
+    /**
+     * @return this {@link Attachments} instance with all mapped values and the linked resource cleared.
+     */
+    Attachments clear();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Connection.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Connection.java
new file mode 100644
index 0000000..01a163a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Connection.java
@@ -0,0 +1,347 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+
+/**
+ * AMQP Connection state container
+ */
+public interface Connection extends Endpoint<Connection> {
+
+    /**
+     * If not already negotiated this method initiates the AMQP protocol negotiation phase of
+     * the connection process sending the {@link AMQPHeader} to the remote peer.  For a client
+     * application this could mean requesting the server to indicate if it supports the version
+     * of the protocol this client speaks.  In rare cases a server could use this to preemptively
+     * send its AMQP header.
+     *
+     * Once a header is sent the remote should respond with the AMQP Header that indicates what
+     * protocol level it supports and if there is a mismatch the the engine will be failed with
+     * a error indicating the protocol support was not successfully negotiated.
+     *
+     * If the engine has a configured SASL layer then by starting the AMQP Header exchange this
+     * will implicitly first attempt the SASL authentication step of the connection process.
+     *
+     * @return this {@link Connection} instance.
+     *
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     */
+    Connection negotiate() throws EngineStateException;
+
+    /**
+     * If not already negotiated this method initiates the AMQP protocol negotiation phase of
+     * the connection process sending the {@link AMQPHeader} to the remote peer.  For a client
+     * application this could mean requesting the server to indicate if it supports the version
+     * of the protocol this client speaks.  In rare cases a server could use this to preemptively
+     * send its AMQP header.
+     *
+     * Once a header is sent the remote should respond with the AMQP Header that indicates what
+     * protocol level it supports and if there is a mismatch the the engine will be failed with
+     * a error indicating the protocol support was not successfully negotiated.
+     *
+     * If the engine has a configured SASL layer then by starting the AMQP Header exchange this
+     * will implicitly first attempt the SASL authentication step of the connection process.
+     *
+     * The provided remote AMQP Header handler will be called once the remote sends its AMQP Header to
+     * the either preemptively or as a response to offered AMQP Header from this peer, even if that has
+     * already happened prior to this call.
+     *
+     * @param remoteAMQPHeaderHandler
+     *      Handler to be called when an AMQP Header response has arrived.
+     *
+     * @return this {@link Connection} instance.
+     *
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     */
+    Connection negotiate(EventHandler<AMQPHeader> remoteAMQPHeaderHandler) throws EngineStateException;
+
+    /**
+     * Performs a tick operation on the connection which checks that Connection Idle timeout processing
+     * is run.  This method is a convenience method that delegates the work to the {@link Engine#tick(long)}
+     * method.
+     *
+     * It is an error to call this method if {@link Connection#tickAuto(ScheduledExecutorService)} was called.
+     *
+     * @param current
+     *      Current time value usually taken from {@link System#nanoTime()}
+     *
+     * @return the absolute deadline in milliseconds to next call tick by/at, or 0 if there is none.
+     *
+     * @throws IllegalStateException if the {@link Engine} is already performing auto tick handling.
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+
+     * @see Engine#tick(long)
+     */
+    long tick(long current) throws IllegalStateException, EngineStateException;
+
+    /**
+     * Convenience method which is the same as calling {@link Engine#tickAuto(ScheduledExecutorService)}.
+     *
+     * @param executor
+     *      The single threaded execution context where all engine work takes place.
+     *
+     * @return this {@link Connection} instance.
+     *
+     * @throws IllegalStateException if the {@link Engine} is already performing auto tick handling.
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     *
+     * @see Engine#tickAuto(ScheduledExecutorService)
+     */
+    Connection tickAuto(ScheduledExecutorService executor);
+
+    /**
+     * @return the local connection state only
+     */
+    ConnectionState getState();
+
+    /**
+     * @return this {@link Connection} as it is the root of the {@link Endpoint} hierarchy.
+     */
+    @Override
+    Connection getParent();
+
+    //----- Operations on local end of this Connection
+
+    /**
+     * @return the Container ID assigned to this Connection
+     */
+    String getContainerId();
+
+    /**
+     * Sets the Container Id to be used when opening this Connection.  The container Id can only
+     * be modified prior to a call to {@link Connection#open()}, once the connection has been
+     * opened locally an error will be thrown if this method is called.
+     *
+     * @param containerId
+     *      The Container Id used for this end of the Connection.
+     *
+     * @return this connection.
+     *
+     * @throws IllegalStateException if the Connection has already been opened.
+     */
+    Connection setContainerId(String containerId) throws IllegalStateException;
+
+    /**
+     * Set the name of the host (either fully qualified or relative) to which this
+     * connection is connecting to.  This information may be used by the remote peer
+     * to determine the correct back-end service to connect the client to. This value
+     * will be sent in the Open performative.
+     *
+     * <b>Note that it is illegal to set the host name to a numeric IP
+     * address or include a port number.</b>
+     *
+     * The host name value can only be modified prior to a call to {@link Connection#open()},
+     * once the connection has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param hostname the RFC1035 compliant host name.
+     *
+     * @return this connection.
+     *
+     * @throws IllegalStateException if the Connection has already been opened.
+     */
+    Connection setHostname(String hostname) throws IllegalStateException;
+
+    /**
+     * @return returns the host name assigned to this Connection.
+     *
+     * @see #setHostname
+     */
+    String getHostname();
+
+    /**
+     * Set the channel max value for this Connection.
+     *
+     * The channel max value can only be modified prior to a call to {@link Connection#open()},
+     * once the connection has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param channelMax
+     *      The value to set for channel max when opening the connection.
+     *
+     * @return this connection.
+     *
+     * @throws IllegalStateException if the Connection has already been opened.
+     */
+    Connection setChannelMax(int channelMax) throws IllegalStateException;
+
+    /**
+     * @return the currently configured channel max for this {@link Connection}
+     */
+    int getChannelMax();
+
+    /**
+     * Sets the maximum frame size allowed for this connection, which is the largest single frame
+     * that the remote can send to this {@link Connection} before it will close the connection with
+     * an error condition indicating the violation.
+     *
+     * The legal range for this value is defined as (512 - 2^32-1) bytes.
+     *
+     * The max frame size value can only be modified prior to a call to {@link Connection#open()},
+     * once the connection has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param maxFrameSize
+     *      The maximum number of bytes allowed for a single
+     *
+     * @return this connection.
+     *
+     * @throws IllegalStateException if the Connection has already been opened.
+     */
+    Connection setMaxFrameSize(long maxFrameSize) throws IllegalStateException;
+
+    /**
+     * @return the currently configured max frame size this connection will accept.
+     */
+    long getMaxFrameSize();
+
+    /**
+     * Set the idle timeout value for this Connection.
+     *
+     * The idle timeout value can only be modified prior to a call to {@link Connection#open()},
+     * once the connection has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param idleTimeout
+     *      The value to set for the idle timeout when opening the connection.
+     *
+     * @return this connection.
+     *
+     * @throws IllegalStateException if the Connection has already been opened.
+     */
+    Connection setIdleTimeout(long idleTimeout) throws IllegalStateException;
+
+    /**
+     * @return the currently configured idle timeout for this {@link Connection}
+     */
+    long getIdleTimeout();
+
+    //----- Session specific APIs for this Connection
+
+    /**
+     * Creates a new Session linked to this Connection
+     *
+     * @return a newly created {@link Session} linked to this {@link Connection}.
+     *
+     * @throws IllegalStateException if the {@link Connection} has already been closed.
+     */
+    Session session() throws IllegalStateException;
+
+    /**
+     * Returns an unmodifiable {@link Set} of Sessions that are tracked by the Connection.
+     *
+     * The {@link Session} instances returned from this method will be locally or remotely open or
+     * both which gives the caller full view of the complete set of known {@link Session} instances.
+     *
+     * @return an unmodifiable {@link Set} of Sessions tracked by this Connection.
+     */
+    Set<Session> sessions();
+
+    //----- View state of remote end of this Connection
+
+    /**
+     * @return the Container Id assigned to the remote end of the Connection.
+     */
+    String getRemoteContainerId();
+
+    /**
+     * @return the host name assigned to the remote end of this Connection.
+     */
+    String getRemoteHostname();
+
+    /**
+     * @return the idle timeout value provided by the remote end of this Connection.
+     */
+    long getRemoteIdleTimeout();
+
+    /**
+     * @return the remote set max frame size limit.
+     */
+    long getRemoteMaxFrameSize();
+
+    /**
+     * @return the remote state (as last communicated)
+     */
+    ConnectionState getRemoteState();
+
+    //----- Remote events for AMQP Connection resources
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Begin frame is received from the remote peer.
+     *
+     * Used to process remotely initiated Sessions. Locally initiated sessions have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote Session creation.
+     *
+     * @param remoteSessionOpenEventHandler
+     *          the EventHandler that will be signaled when a session is remotely opened.
+     *
+     * @return this connection
+     */
+    Connection sessionOpenHandler(EventHandler<Session> remoteSessionOpenEventHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Attach frame is received from the remote peer for a sending link.
+     *
+     * Used to process remotely initiated sending link.  Locally initiated links have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote Receiver creation.
+     * If an event handler for remote sender open is registered on the Session that the link is owned by then
+     * that handler will be invoked instead of this one.
+     *
+     * @param remoteSenderOpenEventHandler
+     *          the EventHandler that will be signaled when a sender link is remotely opened.
+     *
+     * @return this connection
+     */
+    Connection senderOpenHandler(EventHandler<Sender> remoteSenderOpenEventHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Attach frame is received from the remote peer for a receiving link.
+     *
+     * Used to process remotely initiated receiving link.  Locally initiated links have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote Sender creation.
+     * If an event handler for remote receiver open is registered on the Session that the link is owned by then
+     * that handler will be invoked instead of this one.
+     *
+     * @param remoteReceiverOpenEventHandler
+     *          the EventHandler that will be signaled when a receiver link is remotely opened.
+     *
+     * @return this connection
+     */
+    Connection receiverOpenHandler(EventHandler<Receiver> remoteReceiverOpenEventHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Attach frame is received from the remote peer for a transaction
+     * coordination link.
+     *
+     * Used to process remotely initiated transaction manager link.  Locally initiated links have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote {@link TransactionController}
+     * creation.  If an event handler for remote {@link TransactionController} open is registered on the Session that the
+     * link is owned by then that handler will be invoked instead of this one.
+     *
+     * @param remoteTxnManagerOpenEventHandler
+     *          the EventHandler that will be signaled when a {@link TransactionController} link is remotely opened.
+     *
+     * @return this connection
+     */
+    Connection transactionManagerOpenHandler(EventHandler<TransactionManager> remoteTxnManagerOpenEventHandler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/ConnectionState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/ConnectionState.java
new file mode 100644
index 0000000..4157f1c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/ConnectionState.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * Represents the state of an AMQP Connection.
+ */
+public enum ConnectionState {
+    IDLE,
+    ACTIVE,
+    CLOSED,
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/DeliveryTagGenerator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/DeliveryTagGenerator.java
new file mode 100644
index 0000000..e0f9a8b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/DeliveryTagGenerator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.DeliveryTag;
+
+/**
+ * Transfer tag generators can be assigned to {@link Sender} links in order to
+ * allow the link to automatically assign a transfer tag to each outbound delivery.
+ * Depending on the Sender different tag generators can operate in a fashion that is
+ * most efficient for that link such as caching tags for links that will produce a
+ * large number of messages to avoid GC overhead, while for other links simpler
+ * generator types could be used.
+ */
+public interface DeliveryTagGenerator {
+
+    /**
+     * Creates and returns the next {@link DeliveryTag} tag that should be used when
+     * populating an {@link OutgoingDelivery}.
+     *
+     * @return the next {@link DeliveryTag} to use for an {@link OutgoingDelivery}.
+     */
+    DeliveryTag nextTag();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EmptyEnvelope.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EmptyEnvelope.java
new file mode 100644
index 0000000..9caac1c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EmptyEnvelope.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.transport.Performative.PerformativeHandler;
+
+/**
+ * An empty envelope which can be used to drive transport activity when idle.
+ */
+public final class EmptyEnvelope extends IncomingAMQPEnvelope {
+
+    public static final EmptyEnvelope INSTANCE = new EmptyEnvelope();
+
+    public EmptyEnvelope() {
+        super();
+    }
+
+    @Override
+    public String toString() {
+        return "Empty Frame";
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, E context) {
+        // Nothing to do for empty frame.
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Endpoint.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Endpoint.java
new file mode 100644
index 0000000..f14d358
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Endpoint.java
@@ -0,0 +1,329 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.Map;
+
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Represents a conceptual endpoint type used to provide common operations that
+ * all the endpoint types will share.
+ *
+ * @param <E> The {@link Endpoint} type
+ */
+public interface Endpoint<E extends Endpoint<E>> {
+
+    /**
+     * Open the end point locally, sending the Open performative immediately if possible or holding
+     * it until SASL negotiations or the AMQP header exchange and other required performative exchanges
+     * has completed.
+     *
+     * The end point will signal any registered handler of the remote opening the Connection
+     * once the remote performative that signals open completion arrives.
+     *
+     * @return this {@link Endpoint} instance.
+     *
+     * @throws EngineStateException if an error occurs opening the Connection or the Engine is shutdown.
+     */
+    E open() throws EngineStateException;
+
+    /**
+     * Close the end point locally and send the closing performative immediately if possible or
+     * holds it until the Connection / Engine state allows it.  If the engine encounters an error writing
+     * the performative or the engine is in a failed state from a previous error then this method will
+     * throw an exception.  If the engine has been shutdown then this method will close out the local
+     * end of the {@link Endpoint} and clean up any local resources before returning normally.
+     *
+     * @return this {@link Endpoint} instance.
+     *
+     * @throws EngineFailedException if an error occurs closing the end point or the Engine is in a failed state.
+     */
+    E close() throws EngineFailedException;
+
+    /**
+     * @return the {@link Attachments} instance that is associated with this {@link Endpoint}
+     */
+    Attachments getAttachments();
+
+    /**
+     * @return the {@link Engine} which created this {@link Endpoint} instance.
+     */
+    Engine getEngine();
+
+    /**
+     * Gets the parent of this {@link Endpoint} which can be itself for {@link Connection} instance.
+     *
+     * @return the parent of this {@link Endpoint} or itself if this is a {@link Connection};
+     */
+    Endpoint<?> getParent();
+
+    /**
+     * Links a given resource to this {@link Endpoint}.
+     *
+     * @param resource
+     *      The resource to link to this {@link Endpoint}.
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E setLinkedResource(Object resource);
+
+    /**
+     * @return the user set linked resource for this {@link Endpoint} instance.
+     */
+    <T> T getLinkedResource();
+
+    /**
+     * Gets the linked resource (if set) and returns it using the type information
+     * provided to cast the returned value.
+     *
+     * @param <T> The type to cast the linked resource to if one is set.
+     * @param typeClass the type's Class which is used for casting the returned value.
+     *
+     * @return the user set linked resource for this Context instance.
+     *
+     * @throws ClassCastException if the linked resource cannot be cast to the type requested.
+     */
+    <T> T getLinkedResource(Class<T> typeClass);
+
+    //----- Operations on local end of this End Point
+
+    /**
+     * @return the local {@link Endpoint} error, or null if there is none
+     */
+    ErrorCondition getCondition();
+
+    /**
+     * Sets the local {@link ErrorCondition} to be applied to a {@link Endpoint} close.
+     *
+     * @param condition
+     *      The error condition to convey to the remote peer on close of this end point.
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E setCondition(ErrorCondition condition);
+
+    /**
+     * Returns true if this {@link Endpoint} is currently locally open meaning that the {@link Endpoint#open()}
+     * has been called but the {@link Endpoint#close()} has not.
+     *
+     * @return <code>true</code> if the {@link Endpoint} is locally open.
+     *
+     * @see Endpoint#isLocallyClosed()
+     */
+    boolean isLocallyOpen();
+
+    /**
+     * Returns true if this {@link Endpoint} is currently locally closed meaning that a call to the
+     * {@link Endpoint#close} method has occurred.
+     *
+     * @return <code>true</code> if the {@link Endpoint} is locally closed.
+     *
+     * @see Endpoint#isLocallyOpen()
+     */
+    boolean isLocallyClosed();
+
+    /**
+     * Sets the capabilities to be offered on to the remote when this {@link Endpoint} is
+     * opened.
+     *
+     * The offered capabilities value can only be modified prior to a call to {@link Endpoint#open()},
+     * once the {@link Endpoint} has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param capabilities
+     *      The capabilities to be offered to the remote when the {@link Endpoint} is opened.
+     *
+     * @return this {@link Endpoint} instance.
+     *
+     * @throws IllegalStateException if the {@link Endpoint} has already been opened.
+     */
+    E setOfferedCapabilities(Symbol... capabilities) throws IllegalStateException;
+
+    /**
+     * @return the configured capabilities that are offered to the remote when the {@link Endpoint} is opened.
+     */
+    Symbol[] getOfferedCapabilities();
+
+    /**
+     * Sets the capabilities that are desired from the remote when this {@link Endpoint} is
+     * opened.
+     *
+     * The desired capabilities value can only be modified prior to a call to {@link Endpoint#open()},
+     * once the {@link Endpoint} has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param capabilities
+     *      The capabilities desired from the remote when the {@link Endpoint} is opened.
+     *
+     * @return this {@link Endpoint} instance.
+     *
+     * @throws IllegalStateException if the {@link Endpoint} has already been opened.
+     */
+    E setDesiredCapabilities(Symbol... capabilities) throws IllegalStateException;
+
+    /**
+     * @return the configured desired capabilities that are sent to the remote when the Connection is opened.
+     */
+    Symbol[] getDesiredCapabilities();
+
+    /**
+     * Sets the properties to be sent to the remote when this {@link Endpoint} is Opened.
+     *
+     * The {@link Endpoint} properties value can only be modified prior to a call to {@link Endpoint#open()},
+     * once the {@link Endpoint} has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param properties
+     *      The properties that will be sent to the remote when this Connection is opened.
+     *
+     * @return this {@link Endpoint} instance.
+     *
+     * @throws IllegalStateException if the {@link Endpoint} has already been opened.
+     */
+    E setProperties(Map<Symbol, Object> properties) throws IllegalStateException;
+
+    /**
+     * @return the configured properties sent to the remote when this Connection is opened.
+     */
+    Map<Symbol, Object> getProperties();
+
+    //----- Operations on remote end of this End Point
+
+    /**
+     * Returns true if this {@link Endpoint} is currently remotely open meaning that the AMQP performative
+     * that completes the open phase of this {@link Endpoint}'s lifetime has arrived but the performative
+     * that closes it has not.
+     *
+     * @return <code>true</code> if the {@link Endpoint} is remotely open.
+     *
+     * @see Endpoint#isRemotelyClosed()
+     */
+    boolean isRemotelyOpen();
+
+    /**
+     * Returns true if this {@link Endpoint} is currently remotely closed meaning that the AMQP performative
+     * that completes the close phase of this {@link Endpoint}'s lifetime has arrived.
+     *
+     * @return <code>true</code> if the {@link Endpoint} is remotely closed.
+     *
+     * @see Endpoint#isRemotelyOpen()
+     */
+    boolean isRemotelyClosed();
+
+    /**
+     * If the remote has closed this {@link Endpoint} and provided an {@link ErrorCondition} as part
+     * of the closing AMQP performative then this method will return it.
+     *
+     * @return the remote supplied {@link ErrorCondition}, or null if there is none.
+     */
+    ErrorCondition getRemoteCondition();
+
+    /**
+     * @return the capabilities offered by the remote when it opened its end of the {@link Endpoint}.
+     */
+    Symbol[] getRemoteOfferedCapabilities();
+
+    /**
+     * @return the capabilities desired by the remote when it opened its end of the {@link Endpoint}.
+     */
+    Symbol[] getRemoteDesiredCapabilities();
+
+    /**
+     * @return the properties sent by the remote when it opened its end of the {@link Endpoint}.
+     */
+    Map<Symbol, Object> getRemoteProperties();
+
+    //----- Events for AMQP Endpoint resources
+
+    /**
+     * Sets a {@link EventHandler} for when an this {@link Endpoint} is opened locally via a call to {@link Endpoint#open()}
+     *
+     * Typically used by clients for logging or other state update event processing.  Clients should not perform any
+     * blocking calls within this context.  It is an error for the handler to throw an exception and the outcome of
+     * doing so is undefined.
+     *
+     * @param localOpenHandler
+     *      The {@link EventHandler} to notify when this {@link Endpoint} is locally opened.
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E localOpenHandler(EventHandler<E> localOpenHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an this {@link Endpoint} is closed locally via a call to {@link Connection#close()}
+     *
+     * Typically used by clients for logging or other state update event processing.  Clients should not perform any
+     * blocking calls within this context.  It is an error for the handler to throw an exception and the outcome of
+     * doing so is undefined.
+     *
+     * @param localCloseHandler
+     *      The {@link EventHandler} to notify when this {@link Endpoint} is locally closed.
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E localCloseHandler(EventHandler<E> localCloseHandler);
+
+    /**
+     * Sets a EventHandler for when an AMQP Open frame is received from the remote peer.
+     *
+     * Used to process remotely initiated Connections. Locally initiated sessions have their own EventHandler
+     * invoked instead.  This method is typically used by servers to listen for the remote peer to open its
+     * {@link Endpoint}, while a client would listen for the server to open its end of the {@link Endpoint} once
+     * a local open has been performed.
+     *
+     * Typically used by clients as servers will typically listen to some parent resource event handler
+     * to determine if the remote is initiating a resource open.
+     *
+     * @param remoteOpenEventHandler
+     *          the EventHandler that will be signaled when the {@link Endpoint} has been remotely opened
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E openHandler(EventHandler<E> remoteOpenEventHandler);
+
+    /**
+     * Sets a EventHandler for when an AMQP Close frame is received from the remote peer.
+     *
+     * @param remoteCloseEventHandler
+     *          the EventHandler that will be signaled when the {@link Endpoint} is remotely closed.
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E closeHandler(EventHandler<E> remoteCloseEventHandler);
+
+    /**
+     * Sets an {@link EventHandler} that is invoked when the engine that supports this {@link Endpoint} is shutdown
+     * via a call to {@link Engine#shutdown()} which indicates a desire to terminate all engine operations. Any
+     * {@link Endpoint} that has been both locally and remotely closed will not receive this event as it will no longer
+     * be tracked by the parent its parent {@link Endpoint}.
+     *
+     * A typical use of this event would be from a locally closed {@link Endpoint} that is awaiting response from
+     * the remote.  If this event fires then there will never be a remote response to any pending operations and
+     * the client or server instance should react accordingly to clean up any related resources etc.
+     *
+     * @param engineShutdownEventHandler
+     *      the EventHandler that will be signaled when this {@link Endpoint}'s engine is explicitly shutdown.
+     *
+     * @return this {@link Endpoint} instance.
+     */
+    E engineShutdownHandler(EventHandler<Engine> engineShutdownEventHandler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Engine.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Engine.java
new file mode 100644
index 0000000..06647b9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Engine.java
@@ -0,0 +1,299 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineNotWritableException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineShutdownException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtonException;
+
+/**
+ * AMQP Engine interface.
+ */
+public interface Engine extends Consumer<ProtonBuffer> {
+
+    /**
+     * Returns true if the engine is accepting input from the ingestion entry points.
+     * <p>
+     * When false any attempts to write more data into the engine will result in an
+     * error being returned from the write operation.  An engine that has not been
+     * started or that has been failed or shutdown will report as not writable.
+     *
+     * @return true if the engine is current accepting more input.
+     */
+    boolean isWritable();
+
+    /**
+     * @return true if the Engine has entered the running state and is not failed or shutdown.
+     */
+    boolean isRunning();
+
+    /**
+     * @return true if the Engine has been shutdown and is no longer usable.
+     */
+    boolean isShutdown();
+
+    /**
+     * @return true if the Engine has encountered a critical error and cannot produce new data.
+     */
+    boolean isFailed();
+
+    /**
+     * @return the error that caused the {@link Engine} fail and shutdown (or null if not failed).
+     */
+    Throwable failureCause();
+
+    /**
+     * @return the current state of the engine.
+     */
+    EngineState state();
+
+    /**
+     * Gets the {@link Connection} instance that is associated with this {@link Engine} instance.
+     * It is valid for an engine implementation to not return a {@link Connection} instance prior
+     * to the engine having been started.
+     *
+     * @return the {@link Connection} that is linked to this engine instance.
+     */
+    Connection connection();
+
+    /**
+     * Starts the engine and returns the {@link Connection} instance that is bound to this Engine.
+     *
+     * A non-started Engine will not allow ingestion of any inbound data and a Connection linked to
+     * the engine that was obtained from the {@link Engine#connection()} method cannot produce any
+     * outbound data.
+     *
+     * @return the Connection instance that is linked to this {@link Engine}
+     *
+     * @throws EngineStateException if the Engine state has already transition to shutdown or failed.
+     */
+    Connection start() throws EngineStateException;
+
+    /**
+     * Shutdown the engine preventing any future outbound or inbound processing.
+     *
+     * When the engine is shut down any resources, {@link Connection}, {@link Session} or {@link Link}
+     * instances that have an engine shutdown event handler registered will be notified and should react
+     * by locally closing that resource if they wish to ensure that the resource's local close event
+     * handler gets signaled if that resource is not already locally closed.
+     *
+     * @return this {@link Engine}
+     */
+    Engine shutdown();
+
+    /**
+     * Transition the {@link Engine} to a failed state if not already closed or closing.
+     *
+     * If called when the engine has not failed the engine will be transitioned to the failed state
+     * and the method will return an appropriate {@link EngineFailedException} that wraps the given
+     * cause.  If called after the engine was shutdown the method returns an {@link EngineShutdownException}
+     * indicating that the engine was already shutdown.  Repeated calls to this method while the engine
+     * is in the failed state must not alter the original failure error or elicit new engine failed
+     * event notifications.
+     *
+     * @param cause
+     *      The exception that caused the engine to be forcibly transitioned to the failed state.
+     *
+     * @return an {@link EngineStateException} that can be thrown indicating the failure and engine state.
+     */
+    EngineStateException engineFailed(Throwable cause);
+
+    /**
+     * Provide data input for this Engine from some external source.  If the engine is not writable
+     * when this method is called an {@link EngineNotWritableException} will be thrown if unless the
+     * reason for the not writable state is due to engine failure or the engine already having been
+     * shut down in which case the appropriate {@link EngineStateException} will be thrown to indicate
+     * the reason.
+     *
+     * @param input
+     *      The data to feed into to Engine.
+     *
+     * @return this {@link Engine}
+     *
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     */
+    Engine ingest(ProtonBuffer input) throws EngineStateException;
+
+    /**
+     * Provide data input for this Engine from some external source.  If the engine is not writable
+     * when this method is called an {@link EngineNotWritableException} will be thrown if unless the
+     * reason for the not writable state is due to engine failure or the engine already having been
+     * shut down in which case the appropriate {@link EngineStateException} will be thrown to indicate
+     * the reason.
+     *
+     * @param input
+     *      The data to feed into to Engine.
+     *
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     */
+    @Override
+    default void accept(ProtonBuffer input) throws EngineStateException {
+        ingest(input);
+    }
+
+    /**
+     * Prompt the engine to perform idle-timeout/heartbeat handling, and return an absolute
+     * deadline in milliseconds that tick must again be called by/at, based on the provided
+     * current time in milliseconds, to ensure the periodic work is carried out as necessary.
+     * It is an error to call this method if the connection has not been opened.
+     *
+     * A returned deadline of 0 indicates there is no periodic work necessitating tick be called, e.g.
+     * because neither peer has defined an idle-timeout value.
+     *
+     * The provided milliseconds time values should be derived from a monotonic source such as
+     * {@link System#nanoTime()} to prevent wall clock changes leading to erroneous behaviour. Note
+     * that for {@link System#nanoTime()} derived values in particular that the returned deadline
+     * could be a different sign than the originally given value, and so (if non-zero) the returned
+     * deadline should have the current time originally provided subtracted from it in order to
+     * establish a relative time delay to the next deadline.
+     *
+     * Supplying {@link System#currentTimeMillis()} derived values can lead to erroneous behaviour
+     * during wall clock changes and so is not recommended.
+     *
+     * It is an error to call this method if {@link Engine#tickAuto(ScheduledExecutorService)} was called.
+     *
+     * @param currentTime
+     *      the current time of this tick call.
+     *
+     * @return the absolute deadline in milliseconds to next call tick by/at, or 0 if there is none.
+     *
+     * @throws IllegalStateException if the {@link Engine} is already performing auto tick handling.
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     */
+    long tick(long currentTime) throws IllegalStateException, EngineStateException;
+
+    /**
+     * Allows the engine to manage idle timeout processing by providing it the single threaded executor
+     * context where all transport work is done which ensures singled threaded access while removing the
+     * need for the client library or server application to manage calls to the {@link Engine#tick} methods.
+     *
+     * @param executor
+     *      The single threaded execution context where all engine work takes place.
+     *
+     * @throws IllegalStateException if the {@link Engine} is already performing auto tick handling.
+     * @throws EngineStateException if the Engine state precludes accepting new input.
+     *
+     * @return this {@link Engine}
+     */
+    Engine tickAuto(ScheduledExecutorService executor) throws IllegalStateException, EngineStateException;
+
+    /**
+     * Gets the EnginePipeline for this Engine.
+     *
+     * @return the {@link EnginePipeline} for this {@link Engine}.
+     */
+    EnginePipeline pipeline();
+
+    /**
+     * Gets the Configuration for this engine.
+     *
+     * @return the configuration object for this engine.
+     */
+    EngineConfiguration configuration();
+
+    /**
+     * Gets the SASL driver for this engine, if no SASL layer is configured then a
+     * default no-op driver must be returned that indicates this.  The SASL driver provides
+     * the engine with client and server side SASL handshaking support.  An {@link Engine}
+     * implementation can support pluggable SASL drivers or exert tight control over the
+     * driver as it sees fit.
+     *
+     * @return the SASL driver for the engine.
+     */
+    EngineSaslDriver saslDriver();
+
+    //----- Engine event points
+
+    /**
+     * Sets a handler instance that will be notified when data from the engine is ready to
+     * be written to some output sink (socket etc).
+     *
+     * @param output
+     *      The {@link ProtonBuffer} handler instance that performs IO for the engine output.
+     *
+     * @return this {@link Engine}
+     */
+    default Engine outputHandler(EventHandler<ProtonBuffer> output) {
+        return outputHandler((buffer, ioComplete) -> {
+            output.handle(buffer);
+            if (ioComplete != null) {
+                ioComplete.run();
+            }
+        });
+    }
+
+    /**
+     * Sets a {@link Consumer} instance that will be notified when data from the engine is ready to
+     * be written to some output sink (socket etc).
+     *
+     * @param consumer
+     *      The {@link ProtonBuffer} consumer instance that performs IO for the engine output.
+     *
+     * @return this {@link Engine}
+     */
+    default Engine outputConsumer(Consumer<ProtonBuffer> consumer) {
+        return outputHandler((buffer, ioComplete) -> {
+            consumer.accept(buffer);
+            if (ioComplete != null) {
+                ioComplete.run();
+            }
+        });
+    }
+
+    /**
+     * Sets a {@link BiConsumer} instance that will be notified when data from the engine is ready to
+     * be written to some output sink (socket etc).  The {@link Runnable} value provided (if non-null)
+     * should be invoked once the I/O operation has completely successfully.  If the event of an error
+     * writing the data the handler should throw an error or if performed asynchronously the {@link Engine}
+     * should be marked failed via a call to {@link Engine#engineFailed(Throwable)}.
+     *
+     * @param output
+     *      The {@link ProtonBuffer} handler instance that performs IO for the engine output.
+     *
+     * @return this {@link Engine}
+     */
+    Engine outputHandler(BiConsumer<ProtonBuffer, Runnable> output);
+
+    /**
+     * Sets a handler instance that will be notified when the engine encounters a fatal error.
+     *
+     * @param engineFailure
+     *      The {@link ProtonException} handler instance that will be notified if the engine fails.
+     *
+     * @return this {@link Engine}
+     */
+    Engine errorHandler(EventHandler<Engine> engineFailure);
+
+    /**
+     * Sets a handler instance that will be notified when the engine is shut down via a call to the
+     * {@link Engine#shutdown()} method is called.
+     *
+     * @param engineShutdownEventHandler
+     *      The {@link Engine} instance that was was explicitly shut down.
+     *
+     * @return this {@link Engine}
+     */
+    Engine shutdownHandler(EventHandler<Engine> engineShutdownEventHandler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineConfiguration.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineConfiguration.java
new file mode 100644
index 0000000..952d050
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineConfiguration.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBufferAllocator;
+
+/**
+ * Configuration options for the Engine
+ */
+public interface EngineConfiguration {
+
+    /**
+     * Sets the ProtonBufferAllocator used by this Engine.
+     * <p>
+     * When copying data, encoding types or otherwise needing to allocate memory
+     * storage the Engine will use the assigned {@link ProtonBufferAllocator}.
+     * If no allocator is assigned the Engine will use the default allocator.
+     *
+     * @param allocator
+     *      The Allocator instance to use from this {@link Engine}.
+     *
+     * @return this {@link EngineConfiguration} for chaining.
+     */
+    EngineConfiguration setBufferAllocator(ProtonBufferAllocator allocator);
+
+    /**
+     * @return the currently assigned {@link ProtonBufferAllocator}.
+     */
+    ProtonBufferAllocator getBufferAllocator();
+
+    /**
+     * Enables AMQP frame tracing from engine to the system output.  Depending
+     * on the underlying engine composition frame tracing may not be possible
+     * in which case this method will have no effect and the access method
+     * {@link EngineConfiguration#isTraceFrames()} will return false.
+     *
+     * @param traceFrames
+     *      true to enable engine frame tracing, false to disable it.
+     *
+     * @return this {@link EngineConfiguration} for chaining.
+     */
+    EngineConfiguration setTraceFrames(boolean traceFrames);
+
+    /**
+     * @return true if the engine will emit frames to system output.
+     */
+    boolean isTraceFrames();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineFactory.java
new file mode 100644
index 0000000..1db1db0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineFactory.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.engine.impl.ProtonEngineFactory;
+
+/**
+ * Interface used to define the basic mechanisms for creating Engine instances.
+ */
+public interface EngineFactory {
+
+    public static final EngineFactory PROTON = new ProtonEngineFactory();
+
+    /**
+     * Create a new Engine instance with a SASL authentication layer added.  The returned
+     * Engine can either be fully pre-configured for SASL or can require additional user
+     * configuration.
+     *
+     * @return a new Engine instance that can handle SASL authentication.
+     */
+    Engine createEngine();
+
+    /**
+     * Create a new Engine instance that handles only raw AMQP with no SASL layer enabled.
+     *
+     * @return a new raw AMQP aware Engine implementation.
+     */
+    Engine createNonSaslEngine();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineHandler.java
new file mode 100644
index 0000000..0c12a3f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineHandler.java
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.types.security.SaslPerformative;
+import org.apache.qpid.protonj2.types.transport.Performative;
+
+/**
+ * Listen for events generated from the Engine
+ */
+public interface EngineHandler {
+
+    /**
+     * Called when the handler is successfully added to the {@link EnginePipeline} and
+     * will later be initialized before use.
+     *
+     * @param context
+     *      The context that is assigned to this handler.
+     */
+    default void handlerAdded(EngineHandlerContext context) {}
+
+    /**
+     * Called when the handler is successfully removed to the {@link EnginePipeline}.
+     *
+     * @param context
+     *      The context that is assigned to this handler.
+     */
+    default void handlerRemoved(EngineHandlerContext context) {}
+
+    /**
+     * Called when the engine is started to allow handlers to prepare for use based on
+     * the configuration state at start of the engine.  A handler can fail the engine start
+     * by throwing an exception.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     */
+    default void engineStarting(EngineHandlerContext context) {}
+
+    /**
+     * Called when the engine state has changed and handlers may need to update their internal state
+     * to respond to the change or prompt some new work based on the change, e.g state changes from
+     * not writable to writable.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     */
+    default void handleEngineStateChanged(EngineHandlerContext context) {
+        context.fireEngineStateChanged();
+    }
+
+    /**
+     * Called when the engine has transitioned to a failed state and cannot process any additional
+     * input or output.  The handler can free and resources used for normal operations at this point
+     * as the engine is now considered shutdown.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     * @param failure
+     *      The failure that triggered the engine to cease operations.
+     */
+    default void engineFailed(EngineHandlerContext context, EngineFailedException failure) {
+        context.fireFailed(failure);
+    }
+
+    /**
+     * Handle the read of new incoming bytes from a remote sender.  The handler should generally
+     * decode these bytes into an AMQP Performative or SASL Performative based on the current state
+     * of the connection and the handler in question.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     * @param buffer
+     *      The buffer containing the bytes that the engine handler should decode.
+     */
+    default void handleRead(EngineHandlerContext context, ProtonBuffer buffer) {
+        context.fireRead(buffer);
+    }
+
+    /**
+     * Handle the receipt of an incoming AMQP Header or SASL Header based on the current state
+     * of this handler.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     * @param header
+     *      The AMQP Header envelope that wraps the received header instance.
+     */
+    default void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+        context.fireRead(header);
+    }
+
+    /**
+     * Handle the receipt of an incoming SASL performative based on the current state of this handler.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     * @param envelope
+     *      The SASL envelope that wraps the received {@link SaslPerformative}.
+     */
+    default void handleRead(EngineHandlerContext context, SASLEnvelope envelope) {
+        context.fireRead(envelope);
+    }
+
+    /**
+     * Handle the receipt of an incoming AMQP envelope based on the current state of this handler.
+     *
+     * @param context
+     *      The context for this handler which can be used to forward the event to the next handler
+     * @param envelope
+     *      The AMQP envelope that wraps the received {@link Performative}.
+     */
+    default void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope envelope) {
+        context.fireRead(envelope);
+    }
+
+    /**
+     * Handles write of AMQPHeader either by directly writing it to the output target or by
+     * converting it to bytes and firing a write using the {@link ProtonBuffer} based API
+     * in {@link EngineHandlerContext#fireWrite(ProtonBuffer)}
+     *
+     * @param context
+     *      The {@link EngineHandlerContext} associated with this {@link EngineWriteHandler} instance.
+     * @param envelope
+     *      The {@link HeaderEnvelope} instance to write.
+     */
+    default void handleWrite(EngineHandlerContext context, HeaderEnvelope envelope) {
+        context.fireWrite(envelope);
+    }
+
+    /**
+     * Handles write of AMQP performative envelope either by directly writing it to the output target or
+     * by converting it to bytes and firing a write using the {@link ProtonBuffer} based API in
+     * {@link EngineHandlerContext#fireWrite(ProtonBuffer)}
+     *
+     * @param context
+     *      The {@link EngineHandlerContext} associated with this {@link EngineWriteHandler} instance.
+     * @param envelope
+     *      The {@link OutgoingAMQPEnvelope} instance to write.
+     */
+    default void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope envelope) {
+        context.fireWrite(envelope);
+    }
+
+    /**
+     * Handles write of SaslPerformative either by directly writing it to the output target or by
+     * converting it to bytes and firing a write using the {@link ProtonBuffer} based API
+     * in {@link EngineHandlerContext#fireWrite(ProtonBuffer)}
+     *
+     * @param context
+     *      The {@link EngineHandlerContext} associated with this {@link EngineWriteHandler} instance.
+     * @param envelope
+     *      The {@link SASLEnvelope} instance to write.
+     */
+    default void handleWrite(EngineHandlerContext context, SASLEnvelope envelope) {
+        context.fireWrite(envelope);
+    }
+
+    /**
+     * Writes the given bytes to the output target or if no handler in the pipeline handles this
+     * calls the registered output handler of the parent Engine instance.  If not output handler
+     * is found or not handler in the output chain consumes this write the Engine will be failed
+     * as an output sink is required for all low level engine writes.
+     *
+     * @param context
+     *      The {@link EngineHandlerContext} associated with this {@link EngineWriteHandler} instance.
+     * @param buffer
+     *      The {@link ProtonBuffer} whose payload is to be written to the output target.
+     * @param ioComplete
+     *      A {@link Runnable} callback that indicates that the I/O operation is complete
+     */
+    default void handleWrite(EngineHandlerContext context, ProtonBuffer buffer, Runnable ioComplete) {
+        context.fireWrite(buffer, ioComplete);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineHandlerContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineHandlerContext.java
new file mode 100644
index 0000000..c999c6d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineHandlerContext.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+
+/**
+ * Context provided to EngineHandler events to allow further event propagation
+ */
+public interface EngineHandlerContext {
+
+    /**
+     * @return the {@link EngineHandler} that is associated with the context.
+     */
+    EngineHandler handler();
+
+    /**
+     * @return the {@link Engine} where this handler is registered.
+     */
+    Engine engine();
+
+    /**
+     * @return the name that assigned to this {@link EngineHandler} when added to the {@link EnginePipeline}.
+     */
+    String name();
+
+    /**
+     * Fires the engine starting event into the next handler in the {@link EnginePipeline} chain.
+     */
+    void fireEngineStarting();
+
+    /**
+     * Fires the engine state changed event into the next handler in the {@link EnginePipeline} chain.  The
+     * state change events occur after the engine starting event and generally signify that the engine has been
+     * shutdown normally.
+     */
+    void fireEngineStateChanged();
+
+    /**
+     * Fires the {@link Engine} failed event into the next handler in the {@link EnginePipeline} chain.
+     *
+     * @param failure
+     *      The exception that describes the conditions under which the engine failed.
+     */
+    void fireFailed(EngineFailedException failure);
+
+    /**
+     * Fires a read of ProtonBuffer events into the previous handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param buffer
+     *      The {@link ProtonBuffer} that carries the bytes read.
+     */
+    void fireRead(ProtonBuffer buffer);
+
+    /**
+     * Fires a read of HeaderFrame events into the previous handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param header
+     *      The {@link HeaderEnvelope} that carries the header bytes read.
+     */
+    void fireRead(HeaderEnvelope header);
+
+    /**
+     * Fires a read of SASL events into the previous handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param envelope
+     *      The {@link SASLEnvelope} that carries the SASL performative read.
+     */
+    void fireRead(SASLEnvelope envelope);
+
+    /**
+     * Fires a read of IncomingProtocolFrame events into the previous handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param envelope
+     *      The {@link IncomingAMQPEnvelope} that carries the AMQP performative read.
+     */
+    void fireRead(IncomingAMQPEnvelope envelope);
+
+    /**
+     * Fires a write of {@link OutgoingAMQPEnvelope} events into the next handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param envelope
+     *      The {@link OutgoingAMQPEnvelope} that carries the AMQP performative being written.
+     */
+    void fireWrite(OutgoingAMQPEnvelope envelope);
+
+    /**
+     * Fires a write of {@link SASLEnvelope} events into the next handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param envelope
+     *      The {@link SASLEnvelope} that carries the SASL performative being written.
+     */
+    void fireWrite(SASLEnvelope envelope);
+
+    /**
+     * Fires a write of HeaderFrame events into the next handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param envelope
+     *      The {@link HeaderEnvelope} that carries the AMQP Header being written.
+     */
+    void fireWrite(HeaderEnvelope envelope);
+
+    /**
+     * Fires a write of ProtonBuffer events into the next handler in the {@link EnginePipeline} for further
+     * processing.
+     *
+     * @param buffer
+     *      The {@link ProtonBuffer} that carries the bytes being written.
+     * @param ioComplete
+     *      An optional {@link Runnable} callback that is signaled when the I/O completes successfully.
+     */
+    void fireWrite(ProtonBuffer buffer, Runnable ioComplete);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EnginePipeline.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EnginePipeline.java
new file mode 100644
index 0000000..4f48bcf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EnginePipeline.java
@@ -0,0 +1,278 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+
+/**
+ * Pipeline of handlers for Engine work.
+ */
+public interface EnginePipeline {
+
+    /**
+     * @return the {@link Engine} that this pipeline is linked to.
+     */
+    Engine engine();
+
+    /**
+     * Adds the given handler to the front of the pipeline with the given name stored for
+     * later lookup or remove operations.  It is not mandatory that each handler have unique
+     * names although if handlers do share a name the {@link EnginePipeline#remove(String)}
+     * method will only remove them one at a time starting from the first in the pipeline.
+     *
+     * @param name
+     *      The name to assign to the handler
+     * @param handler
+     *      The {@link EngineHandler} to add into the pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     *
+     * @throws IllegalArgumentException if name is null or empty or the handler is null
+     */
+    EnginePipeline addFirst(String name, EngineHandler handler);
+
+    /**
+     * Adds the given handler to the end of the pipeline with the given name stored for
+     * later lookup or remove operations.  It is not mandatory that each handler have unique
+     * names although if handlers do share a name the {@link EnginePipeline#remove(String)}
+     * method will only remove them one at a time starting from the first in the pipeline.
+     *
+     * @param name
+     *      The name to assign to the handler
+     * @param handler
+     *      The {@link EngineHandler} to add into the pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     *
+     * @throws IllegalArgumentException if name is null or empty or the handler is null
+     */
+    EnginePipeline addLast(String name, EngineHandler handler);
+
+    /**
+     * Removes the first {@link EngineHandler} in the pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline removeFirst();
+
+    /**
+     * Removes the last {@link EngineHandler} in the pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline removeLast();
+
+    /**
+     * Removes the first handler that is found in the pipeline that matches the given name.
+     *
+     * @param name
+     *      The name to search for in the pipeline moving from first to last.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline remove(String name);
+
+    /**
+     * Removes the given {@link EngineHandler} from the pipeline if present.
+     *
+     * @param handler
+     *      The handler instance to remove if contained in the pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline remove(EngineHandler handler);
+
+    /**
+     * Finds and returns first handler that is found in the pipeline that matches the given name.
+     *
+     * @param name
+     *      The name to search for in the pipeline moving from first to last.
+     *
+     * @return the {@link EngineHandler} that matches the given name or null if none in the pipeline.
+     */
+    EngineHandler find(String name);
+
+    /**
+     * @return the first {@link EngineHandler} in the pipeline or null if empty.
+     */
+    EngineHandler first();
+
+    /**
+     * @return the last {@link EngineHandler} in the pipeline or null if empty.
+     */
+    EngineHandler last();
+
+    /**
+     * @return the first {@link EngineHandlerContext} in the pipeline or null if empty.
+     */
+    EngineHandlerContext firstContext();
+
+    /**
+     * @return the last {@link EngineHandlerContext} in the pipeline or null if empty.
+     */
+    EngineHandlerContext lastContext();
+
+    /**
+     * Fires an engine starting event to each handler in the pipeline.  Should be used
+     * by the engine implementation to signal its handlers that they should initialize.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireEngineStarting();
+
+    /**
+     * Fires an engine state changed event to each handler in the pipeline.  Should be used
+     * by the engine implementation to signal its handlers that they should respond to the new
+     * engine state, e.g. the engine failed or was shutdown.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireEngineStateChanged();
+
+    /**
+     * Fires a read event consisting of the given {@link ProtonBuffer} into the pipeline starting
+     * from the last {@link EngineHandler} in the pipeline and moving through each until the incoming
+     * work is fully processed.  If the read events reaches the head of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * @param input
+     *      The {@link ProtonBuffer} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireRead(ProtonBuffer input);
+
+    /**
+     * Fires a read event consisting of the given {@link HeaderEnvelope} into the pipeline starting
+     * from the last {@link EngineHandler} in the pipeline and moving through each until the incoming
+     * work is fully processed.  If the read events reaches the head of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * @param header
+     *      The {@link HeaderEnvelope} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireRead(HeaderEnvelope header);
+
+    /**
+     * Fires a read event consisting of the given {@link SASLEnvelope} into the pipeline starting
+     * from the last {@link EngineHandler} in the pipeline and moving through each until the incoming
+     * work is fully processed.  If the read events reaches the head of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * @param envelope
+     *      The {@link SASLEnvelope} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireRead(SASLEnvelope envelope);
+
+    /**
+     * Fires a read event consisting of the given {@link IncomingAMQPEnvelope} into the pipeline starting
+     * from the last {@link EngineHandler} in the pipeline and moving through each until the incoming
+     * work is fully processed.  If the read events reaches the head of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * @param envelope
+     *      The {@link IncomingAMQPEnvelope} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireRead(IncomingAMQPEnvelope envelope);
+
+    /**
+     * Fires a write event consisting of the given {@link HeaderEnvelope} into the pipeline starting
+     * from the first {@link EngineHandler} in the pipeline and moving through each until the outgoing
+     * work is fully processed.  If the write events reaches the tail of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * It is expected that after the fire write method returns the given {@link HeaderEnvelope} will have been
+     * written or if held for later the object must be copied.
+     *
+     * @param envelope
+     *      The {@link HeaderEnvelope} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireWrite(HeaderEnvelope envelope);
+
+    /**
+     * Fires a write event consisting of the given {@link OutgoingAMQPEnvelope} into the pipeline starting
+     * from the first {@link EngineHandler} in the pipeline and moving through each until the outgoing
+     * work is fully processed.  If the write events reaches the tail of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * It is expected that after the fire write method returns the given {@link OutgoingAMQPEnvelope} will have
+     * been written or if held for later the object must be copied.
+     *
+     * When the payload given exceeds the maximum allowed frame size when encoded into an outbound frame the
+     * encoding handler should either throw an error in the case that the performative being written cannot truncate
+     * its payload or should invoke the payload to large handler of the envelope before re-encoding the outbound
+     * performative and truncating the payload.
+     *
+     * @param envelope
+     *      The {@link OutgoingAMQPEnvelope} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireWrite(OutgoingAMQPEnvelope envelope);
+
+    /**
+     * Fires a write event consisting of the given {@link SASLEnvelope} into the pipeline starting
+     * from the first {@link EngineHandler} in the pipeline and moving through each until the outgoing
+     * work is fully processed.  If the write events reaches the tail of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * It is expected that after the fire write method returns the given {@link SASLEnvelope} will have been
+     * written or if held for later the object must be copied.
+     *
+     * @param envelope
+     *      The {@link SASLEnvelope} to inject into the engine pipeline.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireWrite(SASLEnvelope envelope);
+
+    /**
+     * Fires a write event consisting of the given {@link ProtonBuffer} into the pipeline starting
+     * from the first {@link EngineHandler} in the pipeline and moving through each until the outgoing
+     * work is fully processed.  If the write events reaches the tail of the pipeline and is not handled
+     * by any handler an error is thrown and the engine should enter the failed state.
+     *
+     * @param buffer
+     *      The {@link ProtonBuffer} to inject into the engine pipeline.
+     * @param ioComplete
+     *      An optional callback that should be signaled when the underlying transport complete the I/O write
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireWrite(ProtonBuffer buffer, Runnable ioComplete);
+
+    /**
+     * Fires an engine failed event into each {@link EngineHandler} in the pipeline indicating
+     * that the engine is now failed and should not accept or produce new work.
+     *
+     * @param failure
+     *      The cause of the engine failure.
+     *
+     * @return this {@link EnginePipeline}.
+     */
+    EnginePipeline fireFailed(EngineFailedException failure);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineSaslDriver.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineSaslDriver.java
new file mode 100644
index 0000000..09ed8bc
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineSaslDriver.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.engine.sasl.SaslClientContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslOutcome;
+import org.apache.qpid.protonj2.engine.sasl.SaslServerContext;
+
+/**
+ * Driver for the Engine that exposes SASL state and configuration.
+ * <p>
+ * When configured for SASL authentication the SASL driver provides a view of the
+ * current state of the authentication and allows for configuration of the SASL layer
+ * prior to the start of the authentication process.  Once authentication is complete
+ * the driver provides a means of determining the outcome of process.
+ */
+public interface EngineSaslDriver {
+
+    public enum SaslState {
+
+        /**
+         * Engine not started, context can be configured
+         */
+        IDLE,
+
+        /**
+         * Engine started and set configuration in use
+         */
+        AUTHENTICATING,
+
+        /**
+         * Authentication succeeded
+         */
+        AUTHENTICATED,
+
+        /**
+         * Authentication failed
+         */
+        AUTHENTICATION_FAILED,
+
+        /**
+         * No authentication layer configured.
+         */
+        NONE
+
+    }
+
+    /**
+     * Configure this {@link EngineSaslDriver} to operate in client mode and return the associated
+     * {@link SaslClientContext} instance that should be used to complete the SASL negotiation
+     * with the server end.
+     *
+     * @return the SASL client context.
+     *
+     * @throws IllegalStateException if the engine is already in server mode or the engine has not
+     *                               been configure with SASL support.
+     */
+    SaslClientContext client();
+
+    /**
+     * Configure this {@link EngineSaslDriver} to operate in server mode and return the associated
+     * {@link SaslServerContext} instance that should be used to complete the SASL negotiation
+     * with the client end.
+     *
+     * @return the SASL server context.
+     *
+     * @throws IllegalStateException if the engine is already in client mode or the engine has not
+     *                               been configure with SASL support.
+     */
+    SaslServerContext server();
+
+    /**
+     * Returns a SaslState that indicates the current operating state of the SASL
+     * negotiation process or conversely if no SASL layer is configured this method
+     * should return the disabled state.  This method must never return a null result.
+     *
+     * @return the current state of SASL Authentication.
+     */
+    SaslState getSaslState();
+
+    /**
+     * Provides a low level outcome value for the SASL authentication process.
+     * <p>
+     * If the SASL exchange is ongoing or the SASL layer was skipped because a
+     * particular engine configuration allows such behavior then this method
+     * should return null to indicate no SASL outcome is available.
+     *
+     * @return the SASL outcome code that results from authentication
+     */
+    SaslOutcome getSaslOutcome();
+
+    //----- Configuration
+
+    /**
+     * @return the currently configured max frame size allowed for SASL frames.
+     */
+    int getMaxFrameSize();
+
+    /**
+     * Set the maximum frame size the remote can send before an error is indicated.
+     *
+     * @param maxFrameSize
+     *      The maximum allowed frame size from the remote sender.
+     */
+    void setMaxFrameSize(int maxFrameSize);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineState.java
new file mode 100644
index 0000000..30cb703
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EngineState.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * Enumeration of Engine states as visible from the Engine API
+ */
+public enum EngineState {
+
+    /**
+     * The engine has not been started yet and is safe to configure.
+     */
+    IDLE,
+
+    /**
+     * Indicates the engine is in the starting phase and configuration be safe to use now.
+     */
+    STARTING,
+
+    /**
+     * The engine has been started and no changes to configuration are permissible.
+     */
+    STARTED,
+
+    /**
+     * The engine has encountered an error and is no longer usable.
+     */
+    FAILED,
+
+    /**
+     * Engine is shutting down and all pending work should be completed.
+     */
+    SHUTTING_DOWN,
+
+    /**
+     * The engine has been shutdown and can no longer be used.
+     */
+    SHUTDOWN
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EventHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EventHandler.java
new file mode 100644
index 0000000..8a851e4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/EventHandler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * Handler of events from the proton resources.
+ *
+ * @param <E> The type that this handler will provide to the handle method.
+ */
+@FunctionalInterface
+public interface EventHandler<E> {
+
+    /**
+     * Handles the event linked to this EventHandler
+     *
+     * @param target
+     *      The value to be handled.
+     */
+    void handle(E target);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/HeaderEnvelope.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/HeaderEnvelope.java
new file mode 100644
index 0000000..2b678f2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/HeaderEnvelope.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader.HeaderHandler;
+
+/**
+ * Envelope type that carries AMQPHeader instances
+ */
+public class HeaderEnvelope extends PerformativeEnvelope<AMQPHeader> {
+
+    public static final byte HEADER_FRAME_TYPE = (byte) 1;
+
+    public static final HeaderEnvelope SASL_HEADER_ENVELOPE = new HeaderEnvelope(AMQPHeader.getSASLHeader());
+
+    public static final HeaderEnvelope AMQP_HEADER_ENVELOPE = new HeaderEnvelope(AMQPHeader.getAMQPHeader());
+
+    public HeaderEnvelope(AMQPHeader body) {
+        super(HEADER_FRAME_TYPE);
+
+        initialize(body, 0, null);
+    }
+
+    public int getProtocolId() {
+        return getBody().getProtocolId();
+    }
+
+    public int getMajor() {
+        return getBody().getMajor();
+    }
+
+    public int getMinor() {
+        return getBody().getMinor();
+    }
+
+    public int getRevision() {
+        return getBody().getRevision();
+    }
+
+    public boolean isSaslHeader() {
+        return getBody().isSaslHeader();
+    }
+
+    public <E> void invoke(HeaderHandler<E> handler, E context) {
+        getBody().invoke(handler, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/IncomingAMQPEnvelope.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/IncomingAMQPEnvelope.java
new file mode 100644
index 0000000..1693e60
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/IncomingAMQPEnvelope.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.transport.Performative;
+import org.apache.qpid.protonj2.types.transport.Performative.PerformativeHandler;
+
+/**
+ * Frame object that carries an AMQP Performative
+ */
+public class IncomingAMQPEnvelope extends PerformativeEnvelope<Performative> {
+
+    public static final byte AMQP_FRAME_TYPE = (byte) 0;
+
+    private AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> pool;
+
+    IncomingAMQPEnvelope() {
+        this(null);
+    }
+
+    IncomingAMQPEnvelope(AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> pool) {
+        super(AMQP_FRAME_TYPE);
+
+        this.pool = pool;
+    }
+
+    /**
+     * Used to release a Frame that was taken from a Frame pool in order
+     * to make it available for the next input operations.  Once called the
+     * contents of the Frame are invalid and cannot be used again inside the
+     * same context.
+     */
+    public void release() {
+        initialize(null, -1, null);
+
+        if (pool != null) {
+            pool.release(this);
+        }
+    }
+
+    public <E> void invoke(PerformativeHandler<E> handler, E context) {
+        getBody().invoke(handler, getPayload(), getChannel(), context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/IncomingDelivery.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/IncomingDelivery.java
new file mode 100644
index 0000000..cdcf181
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/IncomingDelivery.java
@@ -0,0 +1,317 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * API for an incoming Delivery.
+ */
+public interface IncomingDelivery {
+
+    /**
+     * @return the link that this {@link IncomingDelivery} is bound to.
+     */
+    Receiver getLink();
+
+    /**
+     * Returns the number of bytes currently available for reading form this delivery, which may not be complete yet.
+     * <p>
+     * Note that this value will change as bytes are received, and is in general not equal to the total length of
+     * a delivery, except the point where {@link #isPartial()} returns false and no content has yet been received by
+     * the application.
+     *
+     * @return the number of bytes currently available to read from this delivery.
+     */
+    int available();
+
+    /**
+     * Marks all available bytes as being claimed by the caller meaning that available byte count value can
+     * be returned to the session which can expand the session incoming window to allow more bytes to be
+     * sent from the remote peer.
+     * <p>
+     * This method is useful in the case where the {@link Session} has been configured with a small incoming
+     * capacity and the receiver needs to expand the session window in order to read the entire contents of
+     * a delivery whose payload exceeds the configured session capacity.  The {@link IncomingDelivery}
+     * implementation will track the amount of claimed bytes and ensure that it never releases back more
+     * bytes to the {@link Session} than has actually been received as a whole which allows this method
+     * to be called with each incoming {@link Transfer} frame of a large split framed delivery.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery claimAvailableBytes();
+
+    /**
+     * Returns the current read buffer without copying it effectively consuming all currently available
+     * bytes from this delivery.  If no data is available then this method returns <code>null</code>.
+     *
+     * @return the currently available read bytes for this delivery.
+     */
+    ProtonBuffer readAll();
+
+    /**
+     * Reads bytes from this delivery and writes them into the destination ProtonBuffer reducing the available
+     * bytes by the value of the number of bytes written to the target. The number of bytes written will be the
+     * equal to the writable bytes of the target buffer. The write index of the target buffer will be incremented
+     * by the number of bytes written into it.
+     *
+     * @param buffer
+     *      The target buffer that will be written into.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     *
+     * @throws IndexOutOfBoundsException if the target buffer has more writable bytes than this delivery has readable bytes.
+     */
+    IncomingDelivery readBytes(ProtonBuffer buffer);
+
+    /**
+     * Reads bytes from this delivery and writes them into the destination array starting at the given offset and
+     * continuing for the specified length reducing the available bytes by the value of the number of bytes written
+     * to the target.
+     *
+     * @param array
+     *      The target buffer that will be written into.
+     * @param offset
+     *      The offset into the given array to begin writing.
+     * @param length
+     *      The number of bytes to write to the given array.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     *
+     * @throws IndexOutOfBoundsException if the length is greater than this delivery has readable bytes.
+     */
+    IncomingDelivery readBytes(byte[] array, int offset, int length);
+
+    /**
+     * Configures a default DeliveryState to be used if a received delivery is settled/freed
+     * without any disposition state having been previously applied.
+     *
+     * @param state the default delivery state
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery setDefaultDeliveryState(DeliveryState state);
+
+    /**
+     * @return the default delivery state for this delivery
+     */
+    DeliveryState getDefaultDeliveryState();
+
+    /**
+     * @return the {@link Attachments} instance that is associated with this {@link IncomingDelivery}
+     */
+    Attachments getAttachments();
+
+    /**
+     * Links a given resource to this {@link IncomingDelivery}.
+     *
+     * @param resource
+     *      The resource to link to this {@link IncomingDelivery}.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery setLinkedResource(Object resource);
+
+    /**
+     * @return the user set linked resource for this {@link Endpoint} instance.
+     */
+    <T> T getLinkedResource();
+
+    /**
+     * Gets the linked resource (if set) and returns it using the type information
+     * provided to cast the returned value.
+     *
+     * @param <T> The type to cast the linked resource to if one is set.
+     * @param typeClass the type's Class which is used for casting the returned value.
+     *
+     * @return the user set linked resource for this Context instance.
+     *
+     * @throws ClassCastException if the linked resource cannot be cast to the type requested.
+     */
+    <T> T getLinkedResource(Class<T> typeClass);
+
+    /**
+     * @return the {@link DeliveryTag} assigned to this Delivery.
+     */
+    DeliveryTag getTag();
+
+    /**
+     * @return the {@link DeliveryState} at the local side of this Delivery.
+     */
+    DeliveryState getState();
+
+    /**
+     * Gets the message-format for this Delivery, representing the 32bit value using an int.
+     * <p>
+     * The default value is 0 as per the message format defined in the core AMQP 1.0 specification.<p>
+     * <p>
+     * See the following for more details:<br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-transfer">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-transfer</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-message-format">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-message-format</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#definition-MESSAGE-FORMAT">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#definition-MESSAGE-FORMAT</a><br>
+     *
+     * @return the message-format for this Delivery.
+     */
+    int getMessageFormat();
+
+    /**
+     * Check for whether the delivery is still partial.
+     * <p>
+     * For a receiving Delivery, this means the delivery does not hold
+     * a complete message payload as all the content hasn't been
+     * received yet. Note that an {@link #isAborted() aborted} delivery
+     * will also be considered partial and the full payload won't
+     * be received.
+     * <p>
+     * For a sending Delivery, this means that the application has not marked
+     * the delivery as complete yet.
+     *
+     * @return true if the delivery is partial
+     *
+     * @see #isAborted()
+     */
+    boolean isPartial();
+
+    /**
+     * @return true if the delivery has been aborted.
+     */
+    boolean isAborted();
+
+    /**
+     * @return true if the delivery has been settled locally.
+     */
+    boolean isSettled();
+
+    /**
+     * updates the state of the delivery
+     *
+     * @param state the new delivery state
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery disposition(DeliveryState state);
+
+    /**
+     * Update the delivery with the given disposition if not locally settled
+     * and optionally settles the delivery if not already settled.
+     * <p>
+     * Applies the given delivery state and local settlement value to this delivery
+     * writing a new {@link Disposition} frame if the remote has not already settled
+     * the delivery.  Once locally settled no additional updates to the local
+     * {@link DeliveryState} can be applied and if attempted an {@link IllegalStateException}
+     * will be thrown to indicate this is not possible.
+     *
+     * @param state
+     *      the new delivery state
+     * @param settle
+     *       if true the delivery is settled.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery disposition(DeliveryState state, boolean settle);
+
+    /**
+     * Settles this delivery locally, transmitting a {@link Disposition} frame to the remote
+     * if the remote has not already settled the delivery.  Once locally settled the delivery
+     * will not accept any additional updates to the {@link DeliveryState} via one of the
+     * {@link #disposition(DeliveryState)} or {@link #disposition(DeliveryState, boolean)}
+     * methods.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery settle();
+
+    /**
+     * @return the {@link DeliveryState} at the remote side of this Delivery.
+     */
+    DeliveryState getRemoteState();
+
+    /**
+     * @return true if the delivery has been settled by the remote.
+     */
+    boolean isRemotelySettled();
+
+    /**
+     * Returns the total number of transfer frames that have occurred for the given {@link IncomingDelivery}.
+     *
+     * @return the number of {@link Transfer} frames that this {@link OutgoingDelivery} has initiated.
+     */
+    int getTransferCount();
+
+    //----- Event handlers for the Incoming Delivery
+
+    /**
+     * Handler for incoming deliveries that is called for each incoming {@link Transfer} frame that comprises
+     * either one complete delivery or a chunk of a split framed {@link Transfer}.  The handler should check
+     * that the delivery being read is partial or not and act accordingly, as partial deliveries expect additional
+     * updates as more frames comprising that {@link IncomingDelivery} arrive or the remote aborts the transfer.
+     * <p>
+     * This handler is useful in cases where an incoming delivery is split across many incoming {@link Transfer}
+     * frames either due to a large size or a small max frame size setting and the processing is handed off to some
+     * other resource other than the {@link Receiver} that original handling the first transfer frame.  If the initial
+     * {@link Transfer} carries the entire delivery payload then this event handler will never be called.  Once set
+     * this event handler receiver all updates of incoming delivery {@link Transfer} frames which would otherwise have
+     * been sent to the {@link Receiver#deliveryReadHandler(EventHandler)} instance.
+     *
+     * @param handler
+     *      The handler that will be invoked when {@link Transfer} frames arrive on this receiver link.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery deliveryReadHandler(EventHandler<IncomingDelivery> handler);
+
+    /**
+     * Handler for aborted deliveries that is called if this delivery is aborted by the {@link Sender}.
+     * <p>
+     * This handler is an optional convenience handler that supplements the standard
+     * {@link #deliveryReadHandler(EventHandler)} in cases where the users wishes to break out the
+     * processing of inbound delivery data from abort processing.  If this handler is not set the
+     * {@link Receiver} will call the registered {@link #deliveryAbortedHandler(EventHandler)}
+     * if one is set.
+     *
+     * @param handler
+     *      The handler that will be invoked when {@link Transfer} frames arrive on this receiver link.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery deliveryAbortedHandler(EventHandler<IncomingDelivery> handler);
+
+    /**
+     * Handler for updates to the remote state of incoming deliveries that have previously been received.
+     * <p>
+     * Remote state updates for an {@link IncomingDelivery} can happen when the remote settles a complete
+     * {@link IncomingDelivery} or otherwise modifies the delivery outcome and the user needs to act on those
+     * changes such as a spontaneous update to the {@link DeliveryState}.  If the initial {@link Transfer} of
+     * an incoming delivery already indicates settlement then this handler will never be called.
+     *
+     * @param handler
+     *      The handler that will be invoked when a new remote state update for an {@link IncomingDelivery} arrives on this link.
+     *
+     * @return this {@link IncomingDelivery} instance.
+     */
+    IncomingDelivery deliveryStateUpdatedHandler(EventHandler<IncomingDelivery> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Link.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Link.java
new file mode 100644
index 0000000..ef6cd1d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Link.java
@@ -0,0 +1,441 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+
+/**
+ * Base API for {@link Sender} and {@link Receiver} links.
+ *
+ * @param <L> The link type that this {@link Link} represents, {@link Sender} or {@link Receiver}
+ */
+public interface Link<L extends Link<L>> extends Endpoint<L> {
+
+    /**
+     * Detach this end of the link.
+     *
+     * @return this Link.
+     *
+     * @throws EngineStateException if an error occurs detaching the {@link Link} or the Engine is shutdown.
+     */
+    L detach();
+
+    /**
+     * Returns true if this {@link Link} is currently locally detached meaning the state returned
+     * from {@link Link#getState()} is equal to {@link LinkState#DETACHED}.  A link
+     * is locally detached after a call to {@link Link#detach()}.
+     *
+     * @return true if the link is locally closed.
+     *
+     * @see Link#isLocallyOpen()
+     * @see Link#isLocallyClosed()
+     */
+    boolean isLocallyDetached();
+
+    /**
+     * Returns true if this {@link Link} is currently locally detached or locally closed meaning the
+     * state returned from {@link Link#getState()} is equal to {@link LinkState#DETACHED} or
+     * {@link LinkState#CLOSED}.  A link is locally detached after a call to {@link Link#detach()} and
+     * is locally closed after a call to {@link Link#close()}.
+     *
+     * @return true if the link is locally closed or detached.
+     *
+     * @see Link#isLocallyOpen()
+     * @see Link#isLocallyDetached()
+     * @see Link#isLocallyClosed()
+     */
+    boolean isLocallyClosedOrDetached();
+
+    /**
+     * @return the local link state
+     */
+    LinkState getState();
+
+    /**
+     * Get the credit that is currently available or assigned to this link.
+     *
+     * @return the current link credit.
+     */
+    int getCredit();
+
+    /**
+     * Indicates if the link is draining. For a {@link Sender} link this indicates that the
+     * remote has requested that the Sender transmit deliveries up to the currently available
+     * credit or indicate that it has no more to send.  For a {@link Receiver} this indicates
+     * that the Receiver has requested that the Sender consume its outstanding credit.
+     *
+     * @return true if the {@link Link} is currently marked as draining.
+     */
+    boolean isDraining();
+
+    /**
+     * @return the {@link Role} that this end of the link is performing.
+     */
+    Role getRole();
+
+    /**
+     * @return true if this link is acting in a sender {@link Role}.
+     */
+    boolean isSender();
+
+    /**
+     * @return true if this link is acting in a receiver {@link Role}.
+     */
+    boolean isReceiver();
+
+    /**
+     * @return the parent {@link Connection} for the {@link Link}
+     */
+    Connection getConnection();
+
+    /**
+     * @return the parent {@link Session} of the {@link Link}
+     */
+    Session getSession();
+
+    /**
+     * @return the parent {@link Session} of the {@link Link}
+     */
+    @Override
+    Session getParent();
+
+    /**
+     * @return the link name that is assigned to this {@link Link}
+     */
+    String getName();
+
+    /**
+     * Sets the sender settle mode.
+     * <p>
+     * Should only be called during link set-up, i.e. before calling {@link #open()}. If this endpoint is the
+     * initiator of the link, this method can be used to set a value other than the default.
+     * <p>
+     * If this endpoint is not the initiator, this method should be used to set a local value. According
+     * to the AMQP spec, the application may choose to accept the sender's suggestion
+     * (accessed by calling {@link #getRemoteSenderSettleMode()}) or choose another value. The value
+     * has no effect on Proton, but may be useful to the application at a later point.
+     * <p>
+     * In order to be AMQP compliant the application is responsible for honoring the settlement mode. See {@link Link}.
+     *
+     * @param senderSettleMode
+     *      The {@link SenderSettleMode} that will be set on the local end of this link.
+     *
+     * @return this Link.
+     *
+     * @throws IllegalStateException if the {@link Link} has already been opened.
+     */
+    L setSenderSettleMode(SenderSettleMode senderSettleMode) throws IllegalStateException;
+
+    /**
+     * Gets the local link sender settlement mode.
+     *
+     * @return the local sender settlement mode, or null if none was set.
+     *
+     * @see #setSenderSettleMode(SenderSettleMode)
+     */
+    SenderSettleMode getSenderSettleMode();
+
+    /**
+     * Sets the receiver settle mode.
+     * <p>
+     * Should only be called during link set-up, i.e. before calling {@link #open()}. If this endpoint
+     * is the initiator of the link, this method can be used to set a value other than the default.
+     *
+     * Used in analogous way to {@link #setSenderSettleMode(SenderSettleMode)}
+     *
+     * @param receiverSettleMode
+     *      The {@link ReceiverSettleMode} that will be set on the local end of this link.
+     *
+     * @return this Link.
+     *
+     * @throws IllegalStateException if the {@link Link} has already been opened.
+     */
+    L setReceiverSettleMode(ReceiverSettleMode receiverSettleMode) throws IllegalStateException;
+
+    /**
+     * Gets the local link receiver settlement mode.
+     *
+     * @return the local receiver settlement mode, or null if none was set.
+     *
+     * @see #setReceiverSettleMode(ReceiverSettleMode)
+     */
+    ReceiverSettleMode getReceiverSettleMode();
+
+    /**
+     * Sets the {@link Source} to assign to the local end of this {@link Link}.
+     * <p>
+     * Must be called during link setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param source
+     *      The {@link Source} that will be set on the local end of this link.
+     *
+     * @return this Link.
+     *
+     * @throws IllegalStateException if the {@link Link} has already been opened.
+     */
+    L setSource(Source source) throws IllegalStateException;
+
+    /**
+     * @return the {@link Source} for the local end of this link.
+     */
+    Source getSource();
+
+    /**
+     * Sets the {@link Target} to assign to the local end of this {@link Link}.
+     *
+     * Must be called during link setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param target
+     *      The {@link Target} that will be set on the local end of this link.
+     *
+     * @return this Link.
+     *
+     * @throws IllegalStateException if the {@link Link} has already been opened.
+     */
+    L setTarget(Target target) throws IllegalStateException;
+
+    /**
+     * Sets the {@link Coordinator} target to assign to the local end of this {@link Link}.
+     * <p>
+     * Must be called during link setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param coordinator
+     *      The {@link Coordinator} target that will be set on the local end of this link.
+     *
+     * @return this Link.
+     *
+     * @throws IllegalStateException if the {@link Link} has already been opened.
+     */
+    L setTarget(Coordinator coordinator) throws IllegalStateException;
+
+    /**
+     * Returns the currently set Target for this {@link Link}.  A link target can be either
+     * a {@link Target} type for a {@link Sender} or {@link Receiver} link or if the link is
+     * to be transaction resource then the target type will be a {@link Coordinator} instance.
+     *
+     * @return the link target {@link Terminus} for the local end of this link.
+     */
+    <T extends Terminus> T getTarget();
+
+    /**
+     * Sets the local link max message size, to be conveyed to the peer via the Attach frame
+     * when attaching the link to the session. Null or 0 means no limit.
+     * <p>
+     * Must be called during link setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param maxMessageSize
+     *            the local max message size value, or null to clear. 0 also means no limit.
+     *
+     * @return this Link.
+     *
+     * @throws IllegalStateException if the {@link Link} has already been opened.
+     */
+    L setMaxMessageSize(UnsignedLong maxMessageSize) throws IllegalStateException;
+
+    /**
+     * Gets the local link max message size.
+     *
+     * @return the local max message size, or null if none was set. 0 also means no limit.
+     *
+     * @see #setMaxMessageSize(UnsignedLong)
+     */
+    UnsignedLong getMaxMessageSize();
+
+    //----- View of the state of the link at the remote
+
+    /**
+     * Returns true if this {@link Link} is currently remotely open meaning the state returned
+     * from {@link Link#getRemoteState()} is equal to {@link LinkState#ACTIVE}.  A link
+     * is remotely opened after an {@link Attach} has been received from the remote and before a
+     * {@link Detach} has been received from the remote.
+     *
+     * @return true if the link is remotely open.
+     *
+     * @see Link#isRemotelyClosed()
+     * @see Link#isRemotelyDetached()
+     */
+    @Override
+    boolean isRemotelyOpen();
+
+    /**
+     * Returns true if this {@link Link} is currently remotely closed meaning the state returned
+     * from {@link Link#getRemoteState()} is equal to {@link LinkState#CLOSED}.  A link
+     * is remotely closed after an {@link Detach} has been received from the remote with the close
+     * flag equal to true.
+     *
+     * @return true if the link is remotely closed.
+     *
+     * @see Link#isRemotelyOpen()
+     * @see Link#isRemotelyDetached()
+     */
+    @Override
+    boolean isRemotelyClosed();
+
+    /**
+     * Returns true if this {@link Link} is currently remotely detached meaning the state returned
+     * from {@link Link#getRemoteState()} is equal to {@link LinkState#DETACHED}.  A link
+     * is remotely detached after an {@link Detach} has been received from the remote with the close
+     * flag equal to false.
+     *
+     * @return true if the link is remotely detached.
+     *
+     * @see Link#isRemotelyOpen()
+     * @see Link#isRemotelyClosed()
+     */
+    boolean isRemotelyDetached();
+
+    /**
+     * Returns true if this {@link Link} is currently remotely detached or closed meaning the state
+     * returned from {@link Link#getRemoteState()} is equal to {@link LinkState#DETACHED} or
+     * {@link LinkState#CLOSED}.  A link is remotely detached or closed after a {@link Detach}
+     * has been received from the remote.
+     *
+     * @return true if the link is remotely detached or closed.
+     *
+     * @see Link#isRemotelyOpen()
+     * @see Link#isRemotelyClosed()
+     * @see Link#isRemotelyDetached()
+     */
+    boolean isRemotelyClosedOrDetached();
+
+    /**
+     * @return the source {@link Terminus} for the remote end of this link.
+     */
+    Source getRemoteSource();
+
+    /**
+     * Returns the remote target {@link Terminus} cast to the given type.  This can be used when
+     * the underlying type is known by the caller or as a control to validate the assumption of the
+     * underlying type.
+     * <p>
+     * the currently set Target for this {@link Link}.  A link target can be either a {@link Target}
+     * type for a {@link Sender} or {@link Receiver} link or if the link is to be transaction resource
+     * then the target type will be a {@link Coordinator} instance.
+     *
+     * @param <T>
+     *      The type that the remote {@link Terminus} will be cast to on return.
+     *
+     * @return the source {@link Terminus} for the remote end of this link.
+     */
+    <T extends Terminus> T getRemoteTarget();
+
+    /**
+     * Gets the remote link sender settlement mode, as conveyed from the peer via the Attach frame
+     * when attaching the link to the session.
+     *
+     * @return the sender settlement mode conveyed by the peer, or null if there was none.
+     *
+     * @see #setSenderSettleMode(SenderSettleMode)
+     */
+    SenderSettleMode getRemoteSenderSettleMode();
+
+    /**
+     * Gets the remote link receiver settlement mode, as conveyed from the peer via the Attach frame
+     * when attaching the link to the session.
+     *
+     * @return the sender receiver mode conveyed by the peer, or null if there was none.
+     *
+     * @see #setReceiverSettleMode(ReceiverSettleMode)
+     */
+    ReceiverSettleMode getRemoteReceiverSettleMode();
+
+    /**
+     * Gets the remote link max message size, as conveyed from the peer via the Attach frame
+     * when attaching the link to the session.
+     *
+     * @return the remote max message size conveyed by the peer, or null if none was set. 0 also means no limit.
+     */
+    UnsignedLong getRemoteMaxMessageSize();
+
+    /**
+     * @return the remote link state (as last communicated)
+     */
+    LinkState getRemoteState();
+
+    //----- Remote events for AMQP Link resources
+
+    /**
+     * Sets a {@link EventHandler} for when an this link is detached locally via a call to {@link Link#detach()}
+     *
+     * This is a convenience event that supplements the normal {@link Endpoint#localCloseHandler(EventHandler)}
+     * event point if set.  If no local detached event handler is set the endpoint will route the detached event
+     * to the local closed event handler if set and allow it to process the event in one location.
+     * <p>
+     * Typically used by clients for logging or other state update event processing.  Clients should not perform any
+     * blocking calls within this context.  It is an error for the handler to throw an exception and the outcome of
+     * doing so is undefined.
+     *
+     * @param localDetachHandler
+     *      The {@link EventHandler} to notify when this link is locally detached.
+     *
+     * @return the link for chaining.
+     */
+    L localDetachHandler(EventHandler<L> localDetachHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Detach frame is received from the remote peer for this
+     * {@link Link} which would have been locally opened previously, the Detach from would have been marked
+     * as not having been closed.
+     * <p>
+     * This is a convenience event that supplements the normal {@link Endpoint#closeHandler(EventHandler)}
+     * event point if set.  If no detached event handler is set the endpoint will route the detached event to the
+     * closed event handler if set and allow it to process the event in one location.
+     *
+     * @param remoteDetachHandler
+     *      The {@link EventHandler} to notify when this link is remotely closed.
+     *
+     * @return the {@link Link} for chaining.
+     */
+    L detachHandler(EventHandler<L> remoteDetachHandler);
+
+    /**
+     * Handler for link credit updates that occur after a remote {@link Flow} arrives.
+     *
+     * @param handler
+     *      An event handler that will be signaled when the link credit is updated by a remote flow.
+     *
+     * @return the {@link Link} for chaining.
+     */
+    L creditStateUpdateHandler(EventHandler<L> handler);
+
+    /**
+     * Sets a {@link EventHandler} for when the parent {@link Session} or {@link Connection} of this link is
+     * locally closed.
+     * <p>
+     * Typically used by clients for logging or other state update event processing.  Clients should not perform any
+     * blocking calls within this context.  It is an error for the handler to throw an exception and the outcome of
+     * doing so is undefined.
+     *
+     * @param handler
+     *      The {@link EventHandler} to notify when this link's parent Session is locally closed.
+     *
+     * @return the link for chaining.
+     */
+    L parentEndpointClosedHandler(EventHandler<L> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/LinkCreditState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/LinkCreditState.java
new file mode 100644
index 0000000..04bf8a8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/LinkCreditState.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * State object holding information about the current link credit
+ */
+public interface LinkCreditState {
+
+    /**
+     * The currently available credit for this link
+     *
+     * @return the current amount of link credit
+     */
+    int getCredit();
+
+    /**
+     * The current delivery count value for this link
+     *
+     * @return the current delivery count value for the link.
+     */
+    int getDeliveryCount();
+
+    /**
+     * @return true if the link drain is active.
+     */
+    boolean isDrain();
+
+    /**
+     * @return true if the link has been requested to echo its state.
+     */
+    boolean isEcho();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/LinkState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/LinkState.java
new file mode 100644
index 0000000..ac839f6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/LinkState.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * Represents the state of an AMQP Link.
+ */
+public enum LinkState {
+    IDLE,
+    ACTIVE,
+    DETACHED,
+    CLOSED,
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/OutgoingAMQPEnvelope.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/OutgoingAMQPEnvelope.java
new file mode 100644
index 0000000..2d34bbe
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/OutgoingAMQPEnvelope.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.types.transport.Performative;
+import org.apache.qpid.protonj2.types.transport.Performative.PerformativeHandler;
+
+/**
+ * Frame object that carries an AMQP Performative
+ */
+public class OutgoingAMQPEnvelope extends PerformativeEnvelope<Performative> {
+
+    public static final byte AMQP_FRAME_TYPE = (byte) 0;
+
+    private AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> pool;
+
+    private Consumer<Performative> payloadToLargeHandler = this::defaultPayloadToLargeHandler;
+    private Runnable frameWriteCompleteHandler;
+
+    OutgoingAMQPEnvelope() {
+        this(null);
+    }
+
+    OutgoingAMQPEnvelope(AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> pool) {
+        super(AMQP_FRAME_TYPE);
+
+        this.pool = pool;
+    }
+
+    /**
+     * Configures a handler to be invoked if the payload that is being transmitted with this
+     * performative is to large to allow encoding the frame within the maximum configured AMQP
+     * frame size limit.
+     *
+     * @param payloadToLargeHandler
+     *      Handler that will update the Performative to reflect that more than one frame is required.
+     *
+     * @return this {@link OutgoingAMQPEnvelope} instance.
+     */
+    public OutgoingAMQPEnvelope setPayloadToLargeHandler(Consumer<Performative> payloadToLargeHandler) {
+        if (payloadToLargeHandler != null) {
+            this.payloadToLargeHandler = payloadToLargeHandler;
+        } else {
+            this.payloadToLargeHandler = this::defaultPayloadToLargeHandler;
+        }
+
+        return this;
+    }
+
+    public OutgoingAMQPEnvelope handlePayloadToLarge() {
+        payloadToLargeHandler.accept(getBody());
+        return this;
+    }
+
+    /**
+     * Configures a handler to be invoked when a write operation that was handed off to the I/O layer
+     * has completed indicated that a single frame portion of the payload has been fully written.
+     *
+     * @param frameWriteCompleteHandler
+     *      Runnable handler that will update state or otherwise respond to the write of a frame.
+     *
+     * @return this {@link OutgoingProtocolFrame} instance.
+     */
+    public OutgoingAMQPEnvelope setFrameWriteCompletionHandler(Runnable frameWriteCompleteHandler) {
+        this.frameWriteCompleteHandler = frameWriteCompleteHandler;
+        return this;
+    }
+
+    public OutgoingAMQPEnvelope handleOutgoingFrameWriteComplete() {
+        if (frameWriteCompleteHandler != null) {
+            frameWriteCompleteHandler.run();
+        }
+
+        release();
+
+        return this;
+    }
+
+    /**
+     * Used to release a Frame that was taken from a Frame pool in order
+     * to make it available for the next input operations.  Once called the
+     * contents of the Frame are invalid and cannot be used again inside the
+     * same context.
+     */
+    public void release() {
+        initialize(null, -1, null);
+
+        payloadToLargeHandler = this::defaultPayloadToLargeHandler;
+        frameWriteCompleteHandler = null;
+
+        if (pool != null) {
+            pool.release(this);
+        }
+    }
+
+    public <E> void invoke(PerformativeHandler<E> handler, E context) {
+        getBody().invoke(handler, getPayload(), getChannel(), context);
+    }
+
+    private void defaultPayloadToLargeHandler(Performative performative) {
+        throw new IllegalArgumentException(String.format(
+            "Cannot transmit performative %s with payload larger than max frame size limit", performative));
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/OutgoingDelivery.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/OutgoingDelivery.java
new file mode 100644
index 0000000..85e6ca7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/OutgoingDelivery.java
@@ -0,0 +1,328 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * API for an outgoing Delivery.
+ */
+public interface OutgoingDelivery {
+
+    /**
+     * @return the link that this {@link OutgoingDelivery} is bound to.
+     */
+    Sender getLink();
+
+    /**
+     * @return the {@link Attachments} instance that is associated with this {@link Delivery}
+     */
+    Attachments getAttachments();
+
+    /**
+     * Links a given resource to this {@link Endpoint}.
+     *
+     * @param resource
+     *      The resource to link to this {@link Endpoint}.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     */
+    OutgoingDelivery setLinkedResource(Object resource);
+
+    /**
+     * @return the user set linked resource for this {@link Endpoint} instance.
+     */
+    <T> T getLinkedResource();
+
+    /**
+     * Gets the linked resource (if set) and returns it using the type information
+     * provided to cast the returned value.
+     *
+     * @param <T> The type to cast the linked resource to if one is set.
+     * @param typeClass the type's Class which is used for casting the returned value.
+     *
+     * @return the user set linked resource for this Context instance.
+     *
+     * @throws ClassCastException if the linked resource cannot be cast to the type requested.
+     */
+    <T> T getLinkedResource(Class<T> typeClass);
+
+    /**
+     * Gets the message-format for this Delivery, representing the 32bit value using an int.
+     * <p>
+     * The default value is 0 as per the message format defined in the core AMQP 1.0 specification.<p>
+     * <p>
+     * See the following for more details:<br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-transfer">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-transfer</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-message-format">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-message-format</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#definition-MESSAGE-FORMAT">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#definition-MESSAGE-FORMAT</a><br>
+     *
+     * @return the message-format for this Delivery.
+     */
+    int getMessageFormat();
+
+    /**
+     * Sets the message-format for this Delivery, representing the 32bit value using an integer value. The message format can
+     * only be set@Override prior to the first {@link Transfer} of delivery payload having been written.  If one of the delivery
+     * write methods is called prior to the message format being set then it defaults to the AMQP default format of zero.
+     * <p>
+     * The default value is 0 as per the message format defined in the core AMQP 1.0 specification.<p>
+     * <p>
+     * See the following for more details:<br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-transfer">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-transfer</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-message-format">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-message-format</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format</a><br>
+     * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#definition-MESSAGE-FORMAT">
+     *          http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#definition-MESSAGE-FORMAT</a><br>
+     *
+     * @param messageFormat the message format
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     *
+     * @throws IllegalStateException if the delivery has already written {@link Transfer} frames.
+     */
+    OutgoingDelivery setMessageFormat(int messageFormat);
+
+    /**
+     * @return the {@link DeliveryTag} assigned to this Delivery.
+     */
+    DeliveryTag getTag();
+
+    /**
+     * Sets the delivery tag to assign to this outgoing delivery from the given byte array.
+     *
+     * @param deliveryTag
+     *      a byte array containing the delivery tag to assign to this {@link OutgoingDelivery}
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     *
+     * @throws IllegalStateException if the delivery has already written {@link Transfer} frames.
+     */
+    OutgoingDelivery setTag(byte[] deliveryTag);
+
+    /**
+     * Sets the {@link DeliveryTag} to assign to this outgoing delivery.
+     *
+     * @param deliveryTag
+     *      a byte array containing the delivery tag to assign to this {@link OutgoingDelivery}
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     */
+    OutgoingDelivery setTag(DeliveryTag deliveryTag);
+
+    /**
+     * Check for whether the delivery is still partial.
+     * <p>
+     * For a receiving Delivery, this means the delivery does not hold
+     * a complete message payload as all the content hasn't been
+     * received yet. Note that an {@link #isAborted() aborted} delivery
+     * will also be considered partial and the full payload won't
+     * be received.
+     * <p>
+     * For a sending Delivery, this means that the application has not marked
+     * the delivery as complete yet.
+     *
+     * @return true if the delivery is partial
+     *
+     * @see #isAborted()
+     * @see #isComplete()
+     */
+    boolean isPartial();
+
+    /**
+     * Write the given bytes as the payload of this delivery, no additional writes can occur on this delivery
+     * if the write succeeds in sending all of the given bytes.
+     * <p>
+     * When called the provided buffer is treated as containing the entirety of the transfer payload and the
+     * Transfer(s) that result from this call will result in a final Transfer frame whose more flag is set to
+     * false which tells the remote that no additional data will be sent for this {@link Transfer}.  The
+     * {@link Sender} will output as much of the buffer as possible within the constraints of both the link
+     * credit and the current capacity of the parent {@link Session}.
+     * <p>
+     * The caller must check that all bytes were written and if not they should await updates from the
+     * {@link Sender#creditStateUpdateHandler(EventHandler)} that indicate that the {@link Sender#isSendable()}
+     * has become true again or the caller should check {@link Sender#isSendable()} periodically until it
+     * becomes true once again.
+     *
+     * @param buffer
+     *      The buffer whose contents should be sent.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     *
+     * @throws IllegalStateException if the parent {@link Sender} link becomes inoperable due to closure or failure.
+     */
+    OutgoingDelivery writeBytes(ProtonBuffer buffer);
+
+    /**
+     * Write the given bytes as a portion of the payload of this delivery, additional bytes can be streamed until
+     * the stream complete flag is set to true on a call to {@link #streamBytes(ProtonBuffer, boolean)} or a call
+     * to {@link #writeBytes(ProtonBuffer)} is made.
+     * <p>
+     * The {@link Sender} will output as much of the buffer as possible within the constraints of both the link
+     * credit and the current capacity of the parent {@link Session}.  The caller must check that all bytes were0
+     * written and if not they should await updates from the {@link Sender#creditStateUpdateHandler(EventHandler)}
+     * that indicate that the {@link Sender#isSendable()} has become true again or the caller should check
+     * {@link Sender#isSendable()} periodically until it becomes true once again.
+     * <p>
+     * This method is the same as calling {@link #streamBytes(ProtonBuffer, boolean)} with the complete value set
+     * to false.
+     *
+     * @param buffer
+     *      The buffer whose contents should be sent.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     *
+     * @throws IllegalStateException if the parent {@link Sender} link becomes inoperable due to closure or failure.
+     */
+    OutgoingDelivery streamBytes(ProtonBuffer buffer);
+
+    /**
+     * Write the given bytes as a portion of the payload of this delivery, additional bytes can be streamed until
+     * the stream complete flag is set to true on a call to {@link #streamBytes(ProtonBuffer, boolean)} and the
+     * buffer contents on that send are fully written.
+     * <p>
+     * The {@link Sender} will output as much of the buffer as possible within the constraints of both the link
+     * credit and the current capacity of the parent {@link Session}.  The caller must check that all bytes were0
+     * written and if not they should await updates from the {@link Sender#creditStateUpdateHandler(EventHandler)}
+     * that indicate that the {@link Sender#isSendable()} has become true again or the caller should check
+     * {@link Sender#isSendable()} periodically until it becomes true once again.
+     *
+     * @param buffer
+     *      The buffer whose contents should be sent.
+     * @param complete
+     *      When true the delivery is marked complete and no further bytes can be written.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     *
+     * @throws IllegalStateException if the parent {@link Sender} link becomes inoperable due to closure or failure.
+     */
+    OutgoingDelivery streamBytes(ProtonBuffer buffer, boolean complete);
+
+    /**
+     * @return true if the delivery has been aborted.
+     */
+    boolean isAborted();
+
+    /**
+     * Aborts the outgoing delivery if not already settled.
+     *
+     * @return this delivery.
+     */
+    OutgoingDelivery abort();
+
+    /**
+     * @return the {@link DeliveryState} at the local side of this Delivery.
+     */
+    DeliveryState getState();
+
+    /**
+     * updates the state of the delivery
+     *
+     * @param state the new delivery state
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     */
+    OutgoingDelivery disposition(DeliveryState state);
+
+    /**
+     * Update the delivery with the given disposition if not locally settled and optionally
+     * settles the delivery if not already settled.
+     * <p>
+     * The action taken by this method depends on the state of the {@link OutgoingDelivery}
+     * at the time it is called.
+     * <p>
+     * If there has yet to be any writes from this delivery the delivery state and settlement
+     * value is cached and applied to the first (or only) write of payload from this delivery.
+     * If however a write has already been performed than this method result in a {@link Disposition}
+     * frame being sent to the remote with the given delivery state and settlement value.  Once
+     * the delivery is marked as settled any future call to this method will do nothing if the
+     * requested disposition and settlement is the same however if a new state is applied which
+     * cannot be conveyed due to having already locally settling the {@link OutgoingDelivery} than
+     * an {@link IllegalStateException} is thrown to indicate that request is not valid.
+     *
+     * @param state
+     *      the new delivery state
+     * @param settle
+     *       if true the delivery is settled.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     */
+    OutgoingDelivery disposition(DeliveryState state, boolean settle);
+
+    /**
+     * @return true if the delivery has been settled locally.
+     */
+    boolean isSettled();
+
+    /**
+     * Settles this delivery if not already settled.  Once settled locally no further updates
+     * to the delivery state can be applied.  If called prior to the first write of payload
+     * bytes the settlement state is cached and transmitted within the first {@link Transfer}
+     * frame of this {@link OutgoingDelivery}.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     */
+    OutgoingDelivery settle();
+
+    /**
+     * @return true if the delivery has been settled by the remote.
+     */
+    boolean isRemotelySettled();
+
+    /**
+     * @return the {@link DeliveryState} at the remote side of this Delivery.
+     */
+    DeliveryState getRemoteState();
+
+    /**
+     * Returns the total number of transfer frames that have occurred for the given {@link OutgoingDelivery}.
+     * If the {@link OutgoingDelivery} has yet to have any of its write methods called this value will read
+     * zero.  Aborting a transfer after any {@link Transfer} frames have been written will not result in an
+     * addition recorded {@link Transfer} write.
+     *
+     * @return the number of {@link Transfer} frames that this {@link OutgoingDelivery} has initiated.
+     */
+    int getTransferCount();
+
+    /**
+     * Handler for updates to the remote state of outgoing deliveries that have begun transferring frames.
+     * <p>
+     * Remote state updates for an {@link OutgoingDelivery} can happen when the remote settles a complete
+     * {@link OutgoingDelivery} or otherwise modifies the delivery outcome and the user needs to act on those
+     * changes such as a spontaneous update to the {@link DeliveryState}.  If the initial {@link Transfer} of
+     * an outgoing delivery already indicates settlement then this handler will never be called.
+     *
+     * @param handler
+     *      The handler that will be invoked when a new remote state update for an {@link OutgoingDelivery} arrives on this link.
+     *
+     * @return this {@link OutgoingDelivery} instance.
+     */
+    OutgoingDelivery deliveryStateUpdatedHandler(EventHandler<OutgoingDelivery> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/PerformativeEnvelope.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/PerformativeEnvelope.java
new file mode 100644
index 0000000..79b8d46
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/PerformativeEnvelope.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Base class for envelope types that travel through the engine.
+ *
+ * @param <V> The type of body that this {@link PerformativeEnvelope} will carry.
+ */
+public abstract class PerformativeEnvelope<V> {
+
+    private final byte frameType;
+
+    private V body;
+    private int channel;
+    private ProtonBuffer payload;
+
+    protected PerformativeEnvelope(byte frameType) {
+        this.frameType = frameType;
+    }
+
+    PerformativeEnvelope<V> initialize(V body, int channel, ProtonBuffer payload) {
+        this.body = body;
+        this.channel = channel;
+        this.payload = payload;
+
+        return this;
+    }
+
+    /**
+     * @return the decoded body of the performative that this envelope carries..
+     */
+    public V getBody() {
+        return body;
+    }
+
+    /**
+     * @return the channel that the wrapped body and payload was sent on
+     */
+    public int getChannel() {
+        return channel;
+    }
+
+    /**
+     * @return the frame type that is assigned to this envelope
+     */
+    public byte getFrameType() {
+        return frameType;
+    }
+
+    /**
+     * @return the binary payload that was delivered with this envelope
+     */
+    public ProtonBuffer getPayload() {
+        return payload;
+    }
+
+    @Override
+    public String toString() {
+        return "PerformativeEnvelope:[" + body + ", " + channel + ", " + ", " + payload + "]";
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Receiver.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Receiver.java
new file mode 100644
index 0000000..cb49e66
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Receiver.java
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.Collection;
+import java.util.function.Predicate;
+
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * AMQP Receiver API
+ */
+public interface Receiver extends Link<Receiver> {
+
+    /**
+     * Adds the given amount of credit for the {@link Receiver}.
+     *
+     * @param additionalCredit
+     *      the new amount of credits to add.
+     *
+     * @return this {@link Receiver}
+     *
+     * @throws IllegalArgumentException if the credit amount is negative.
+     */
+    Receiver addCredit(int additionalCredit);
+
+    /**
+     * Initiate a drain of all remaining credit of this {@link Receiver} link.
+     *
+     * @return true if a drain was started or false if the link already had no credit to drain.
+     *
+     * @throws IllegalStateException if an existing drain attempt is incomplete.
+     */
+    boolean drain();
+
+    /**
+     * Initiate a drain of the given credit from this this {@link Receiver} link.  If the credit
+     * given is greater than the current link credit the current credit is increased, however if
+     * the amount of credit given is less that the current amount of link credit an exception is
+     * thrown.
+     *
+     * @param credit
+     *      The amount of credit that should be requested to be drained from this link.
+     *
+     * @return true if a drain was started or false if the value is zero and the link had no credit.
+     *
+     * @throws IllegalStateException if an existing drain attempt is incomplete.
+     * @throws IllegalArgumentException if the credit value given is less than the current value.
+     */
+    boolean drain(int credit);
+
+    /**
+     * Configures a default DeliveryState to be used if a received delivery is settled/freed
+     * without any disposition state having been previously applied.
+     *
+     * @param state the default delivery state
+     *
+     * @return this {@link Receiver} for chaining.
+     */
+    Receiver setDefaultDeliveryState(DeliveryState state);
+
+    /**
+     * @return the default delivery state for this delivery
+     */
+    DeliveryState getDefaultDeliveryState();
+
+    /**
+     * For each unsettled outgoing delivery that is pending in the {@link Receiver} apply the given predicate
+     * and if it matches then apply the given delivery state and settled value to it.
+     *
+     * @param filter
+     *      The predicate to apply to each unsettled delivery to test for a match.
+     * @param state
+     *      The new {@link DeliveryState} to apply to any matching outgoing deliveries.
+     * @param settle
+     *      Boolean indicating if the matching unsettled deliveries should be settled.
+     *
+     * @return this {@link Receiver} for chaining
+     */
+    Receiver disposition(Predicate<IncomingDelivery> filter, DeliveryState state, boolean settle);
+
+    /**
+     * For each unsettled outgoing delivery that is pending in the {@link Receiver} apply the given predicate
+     * and if it matches then settle the delivery.
+     *
+     * @param filter
+     *      The predicate to apply to each unsettled delivery to test for a match.
+     *
+     * @return this {@link Receiver} for chaining
+     */
+    Receiver settle(Predicate<IncomingDelivery> filter);
+
+    /**
+     * Retrieves the list of unsettled deliveries for this {@link Receiver} link which have yet to be settled
+     * on this end of the link.  When the {@link IncomingDelivery} is settled by the receiver the value will
+     * be removed from the collection.
+     *
+     * The {@link Collection} returned from this method is a copy of the internally maintained data and is
+     * not modifiable.  The caller should use this method judiciously to avoid excess GC overhead.
+     *
+     * @return a collection of unsettled deliveries or an empty list if no pending deliveries are outstanding.
+     */
+    Collection<IncomingDelivery> unsettled();
+
+    /**
+     * @return true if there are unsettled deliveries for this {@link Receiver} link.
+     */
+    boolean hasUnsettled();
+
+    //----- Event handlers for the Receiver
+
+    /**
+     * Handler for incoming deliveries that is called for each incoming {@link Transfer} frame that comprises
+     * either one complete delivery or a chunk of a split framed {@link Transfer}.  The handler should check
+     * that the delivery being read is partial or not and act accordingly, as partial deliveries expect additional
+     * updates as more frames comprising that {@link IncomingDelivery} arrive or the remote aborts the transfer.
+     *
+     * @param handler
+     *      The handler that will be invoked when {@link Transfer} frames arrive on this receiver link.
+     *
+     * @return this receiver
+     */
+    Receiver deliveryReadHandler(EventHandler<IncomingDelivery> handler);
+
+    /**
+     * Handler for aborted deliveries that is called for each aborted in-progress delivery.
+     * <p>
+     * This handler is an optional convenience handler that supplements the standard
+     * {@link #deliveryReadHandler(EventHandler)} in cases where the users wishes to break out the
+     * processing of inbound delivery data from abort processing.  If this handler is not set the
+     * {@link Receiver} will call the registered {@link #deliveryAbortedHandler(EventHandler)}
+     * if one is set.
+     *
+     * @param handler
+     *      The handler that will be invoked when {@link Transfer} frames arrive on this receiver link.
+     *
+     * @return this receiver
+     */
+    Receiver deliveryAbortedHandler(EventHandler<IncomingDelivery> handler);
+
+    /**
+     * Handler for updates to the remote state of incoming deliveries that have previously been received.
+     * <p>
+     * Remote state updates for an {@link IncomingDelivery} can happen when the remote settles a complete
+     * {@link IncomingDelivery} or otherwise modifies the delivery outcome and the user needs to act on those
+     * changes such as a spontaneous update to the {@link DeliveryState}.
+     *
+     * @param handler
+     *      The handler that will be invoked when a new remote state update for an {@link IncomingDelivery} arrives on this link.
+     *
+     * @return this receiver
+     */
+    Receiver deliveryStateUpdatedHandler(EventHandler<IncomingDelivery> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/SASLEnvelope.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/SASLEnvelope.java
new file mode 100644
index 0000000..ea87686
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/SASLEnvelope.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.security.SaslPerformative;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+
+/**
+ * Frame object containing a SASL performative
+ */
+public class SASLEnvelope extends PerformativeEnvelope<SaslPerformative>{
+
+    public static final byte SASL_FRAME_TYPE = (byte) 1;
+
+    public SASLEnvelope(SaslPerformative performative) {
+        this(performative, null);
+    }
+
+    public SASLEnvelope(SaslPerformative performative, ProtonBuffer payload) {
+        super(SASL_FRAME_TYPE);
+
+        initialize(performative, 0, payload);
+    }
+
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        getBody().invoke(handler, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Sender.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Sender.java
new file mode 100644
index 0000000..ac2036a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Sender.java
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.Collection;
+import java.util.function.Predicate;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * AMQP Sender API
+ */
+public interface Sender extends Link<Sender> {
+
+    /**
+     * Called when the {@link Receiver} has requested a drain of credit and the sender
+     * has sent all available messages.
+     *
+     * @return this {@link Sender} instance.
+     *
+     * @throws IllegalStateException if the link is not draining currently.
+     */
+    Sender drained();
+
+    /**
+     * Checks if the sender has credit and the session window allows for any bytes to be written currently.
+     *
+     * @return true if the link has credit and the session window allows for any bytes to be written.
+     */
+    boolean isSendable();
+
+    /**
+     * Gets the current {@link OutgoingDelivery} for this {@link Sender} if one is available.
+     * <p>
+     * The sender only tracks a current delivery in the case that the next method has bee called
+     * and if any bytes are written to the delivery using the streaming based API
+     * {@link OutgoingDelivery#streamBytes(ProtonBuffer)} which allows for later writing of additional
+     * bytes to the delivery.  Once the method {@link OutgoingDelivery#writeBytes(ProtonBuffer)} is
+     * called the final {@link Transfer} is written indicating that the delivery is complete and the
+     * current delivery value is reset.  An outgoing delivery that is being streamed may also
+     * be completed by calling the {@link OutgoingDelivery#abort()} method.
+     *
+     * @return the current active outgoing delivery or null if there is no current delivery.
+     */
+    OutgoingDelivery current();
+
+    /**
+     * When there has been no deliveries so far or the current delivery has reached a complete state this
+     * method updates the current delivery to a new instance and returns that value.  If the current
+     * {@link OutgoingDelivery} has not been completed by either calling the
+     * {@link OutgoingDelivery#writeBytes(ProtonBuffer)} or the {@link OutgoingDelivery#abort()} method then
+     * this method will throw an exception to indicate the sender state cannot allow a new delivery to be started.
+     *
+     * @return a new delivery instance unless the current delivery is not complete.
+     *
+     * @throws IllegalStateException if the current delivery has not been marked complete.
+     */
+    OutgoingDelivery next();
+
+    /**
+     * For each unsettled outgoing delivery that is pending in the {@link Sender} apply the given predicate
+     * and if it matches then apply the given delivery state and settled value to it.
+     *
+     * @param filter
+     *      The predicate to apply to each unsettled delivery to test for a match.
+     * @param state
+     *      The new {@link DeliveryState} to apply to any matching outgoing deliveries.
+     * @param settle
+     *      Boolean indicating if the matching unsettled deliveries should be settled.
+     *
+     * @return this {@link Sender} instance.
+     */
+    Sender disposition(Predicate<OutgoingDelivery> filter, DeliveryState state, boolean settle);
+
+    /**
+     * For each unsettled outgoing delivery that is pending in the {@link Sender} apply the given predicate
+     * and if it matches then settle the delivery.
+     *
+     * @param filter
+     *      The predicate to apply to each unsettled delivery to test for a match.
+     *
+     * @return this {@link Sender} instance.
+     */
+    Sender settle(Predicate<OutgoingDelivery> filter);
+
+    /**
+     * Retrieves the list of unsettled deliveries sent from this {@link Sender}.  The deliveries in the {@link Collection}
+     * cannot be written to but can have their settled state and disposition updated.  Only when this {@link Sender}
+     * settles on its end are the {@link OutgoingDelivery} instances removed from the unsettled {@link Collection}.
+     *
+     * The {@link Collection} returned from this method is a copy of the internally maintained data and is
+     * not modifiable.  The caller should use this method judiciously to avoid excess GC overhead.
+     *
+     * @return a collection of unsettled deliveries or an empty collection if no pending deliveries are outstanding.
+     */
+    Collection<OutgoingDelivery> unsettled();
+
+    /**
+     * @return true if there are unsettled deliveries for this {@link Sender} link.
+     */
+    boolean hasUnsettled();
+
+    /**
+     * Configures a {@link DeliveryTagGenerator} that will be used to create and set a {@link DeliveryTag}
+     * value on each new {@link OutgoingDelivery} that is created and returned from the {@link Sender#next()}
+     * method.
+     *
+     * @param generator
+     *      The {@link DeliveryTagGenerator} to use to create automatic {@link DeliveryTag} values.
+     *
+     * @return this {@link Sender} instance.
+     */
+    Sender setDeliveryTagGenerator(DeliveryTagGenerator generator);
+
+    /**
+     * @return the currently configured {@link DeliveryTagGenerator} for this {@link Sender}.
+     */
+    DeliveryTagGenerator getDeliveryTagGenerator();
+
+    //----- Event handlers for the Sender
+
+    /**
+     * Handler for updates for deliveries that have previously been sent.
+     *
+     * Updates can happen when the remote settles or otherwise modifies the delivery and the
+     * user needs to act on those changes.
+     *
+     * @param handler
+     *      The handler that will be invoked when a new update delivery arrives on this link.
+     *
+     * @return this {@link Sender} instance.
+     */
+    Sender deliveryStateUpdatedHandler(EventHandler<OutgoingDelivery> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Session.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Session.java
new file mode 100644
index 0000000..574af9c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Session.java
@@ -0,0 +1,228 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.Set;
+
+/**
+ * AMQP Session interface
+ */
+public interface Session extends Endpoint<Session> {
+
+    /**
+     * @return the local session state
+     */
+    SessionState getState();
+
+    /**
+     * @return the parent {@link Connection} for this Session.
+     */
+    Connection getConnection();
+
+    /**
+     * @return the parent {@link Connection} of the {@link Link}
+     */
+    @Override
+    Connection getParent();
+
+    /**
+     * Returns a {@link Set} of all {@link Sender} and {@link Receiver} instances that are being tracked by
+     * this {@link Session}.
+     *
+     * @return a set of Sender and Receiver instances tracked by this session.
+     */
+    Set<Link<?>> links();
+
+    /**
+     * Returns a {@link Set} of {@link Sender} instances that are being tracked by this {@link Session}.
+     *
+     * @return a set of Sender instances tracked by this session.
+     */
+    Set<? extends Sender> senders();
+
+    /**
+     * Returns a {@link Set} of {@link Receiver} instances that are being tracked by this {@link Session}.
+     *
+     * @return a set of Receiver instances tracked by this session.
+     */
+    Set<? extends Receiver> receivers();
+
+    //----- Session sender and receiver factory methods
+
+    /**
+     * Create a new {@link Sender} link using the provided name.
+     *
+     * @param name
+     *      The name to assign to the created {@link Sender}
+     *
+     * @return a newly created {@link Sender} instance.
+     *
+     * @throws IllegalStateException if the {@link Session} has already been closed.
+     */
+    Sender sender(String name) throws IllegalStateException;
+
+    /**
+     * Create a new {@link Receiver} link using the provided name
+     *
+     * @param name
+     *      The name to assign to the created {@link Receiver}
+     *
+     * @return a newly created {@link Receiver} instance.
+     *
+     * @throws IllegalStateException if the {@link Session} has already been closed.
+     */
+    Receiver receiver(String name) throws IllegalStateException;
+
+    /**
+     * Create a new {@link TransactionController} using the provided name.
+     *
+     * @param name
+     *      The name to assign to the created {@link TransactionController}
+     *
+     * @return a newly created {@link TransactionController} instance.
+     *
+     * @throws IllegalStateException if the {@link Session} has already been closed.
+     */
+    TransactionController coordinator(String name) throws IllegalStateException;
+
+    //----- Configure the local end of the Session
+
+    /**
+     * Sets the maximum number of bytes this session can be sent from the remote.
+     *
+     * @param incomingCapacity
+     *      maximum number of incoming bytes this session will allow
+     *
+     * @return this {@link Session} instance.
+     *
+     * @throws IllegalStateException if the {@link Session} has already been closed.
+     */
+    Session setIncomingCapacity(int incomingCapacity) throws IllegalStateException;
+
+    /**
+     * @return the current incoming capacity of this session.
+     */
+    int getIncomingCapacity();
+
+    /**
+     * @return the remaining session capacity based on how many bytes are currently pending,
+     */
+    int getRemainingIncomingCapacity();
+
+    /**
+     * Sets the maximum number of bytes this session can be write before blocking additional
+     * sends until the written bytes are known to have been flushed to the write.  This limit
+     * is intended to deal with issues of memory allocation when the I/O layer allows for
+     * asynchronous writes and finer grained control over the pending write buffers is needed.
+     *
+     * @param outgoingCapacity
+     *      maximum number of outgoing bytes this session will allow before stopping senders from sending.
+     *
+     * @return this {@link Session} instance.
+     *
+     * @throws IllegalStateException if the {@link Session} has already been closed.
+     */
+    Session setOutgoingCapacity(int outgoingCapacity) throws IllegalStateException;
+
+    /**
+     * @return the current outgoing capacity limit of this session.
+     */
+    int getOutgoingCapacity();
+
+    /**
+     * @return the remaining session outgoing capacity based on how many bytes are currently pending,
+     */
+    int getRemainingOutgoingCapacity();
+
+    /**
+     * Set the handle max value for this Session.
+     *
+     * The handle max value can only be modified prior to a call to {@link Session#open()},
+     * once the session has been opened locally an error will be thrown if this method
+     * is called.
+     *
+     * @param handleMax
+     *      The value to set for handle max when opening the session.
+     *
+     * @return this {@link Session} instance.
+     *
+     * @throws IllegalStateException if the Session has already been opened.
+     */
+    Session setHandleMax(long handleMax) throws IllegalStateException;
+
+    /**
+     * @return the currently configured handle max for this {@link Session}
+     */
+    long getHandleMax();
+
+    //----- View the remote end of the Session configuration
+
+    /**
+     * @return the remote session state (as last communicated)
+     */
+    SessionState getRemoteState();
+
+    //----- Remote events for AMQP Session resources
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Attach frame is received from the remote peer for a sending link.
+     *
+     * Used to process remotely initiated sending link.  Locally initiated links have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote Receiver creation.
+     * If an event handler for remote sender open is registered on this Session for a link scoped to it then
+     * this handler will be invoked instead of the variant in the Connection API.
+     *
+     * @param remoteSenderOpenEventHandler
+     *          the EventHandler that will be signaled when a sender link is remotely opened.
+     *
+     * @return this session for chaining
+     */
+    Session senderOpenHandler(EventHandler<Sender> remoteSenderOpenEventHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Attach frame is received from the remote peer for a receiving link.
+     *
+     * Used to process remotely initiated receiving link.  Locally initiated links have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote Sender creation.
+     * If an event handler for remote sender open is registered on this Session for a link scoped to it then
+     * this handler will be invoked instead of the variant in the Connection API.
+     *
+     * @param remoteReceiverOpenEventHandler
+     *          the EventHandler that will be signaled when a receiver link is remotely opened.
+     *
+     * @return this session for chaining
+     */
+    Session receiverOpenHandler(EventHandler<Receiver> remoteReceiverOpenEventHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when an AMQP Attach frame is received from the remote peer for a transaction
+     * coordination link.
+     *
+     * Used to process remotely initiated transaction manager link.  Locally initiated links have their own EventHandler
+     * invoked instead.  This method is Typically used by servers to listen for remote {@link TransactionController}
+     * creation.  If an event handler for remote {@link TransactionController} open is registered on this Session for a
+     * {@link TransactionController} scoped to it then this handler will be invoked instead of the variant in the
+     * {@link Connection} API.
+     *
+     * @param remoteTxnManagerOpenEventHandler
+     *          the EventHandler that will be signaled when a {@link TransactionController} link is remotely opened.
+     *
+     * @return this session for chaining
+     */
+    Session transactionManagerOpenHandler(EventHandler<TransactionManager> remoteTxnManagerOpenEventHandler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/SessionState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/SessionState.java
new file mode 100644
index 0000000..17e7cb5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/SessionState.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+/**
+ * Represents the state of an AMQP Session.
+ */
+public enum SessionState {
+    IDLE,
+    ACTIVE,
+    CLOSED,
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Transaction.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Transaction.java
new file mode 100644
index 0000000..d896253
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/Transaction.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * A Transaction object that hold information and context for a single {@link Transaction}.
+ *
+ * @param <E> The parent of this Transaction either a {@link TransactionController} or {@link TransactionManager}
+ */
+public interface Transaction<E extends Endpoint<?>> {
+
+    public enum DischargeState {
+        NONE,
+        COMMIT,
+        ROLLBACK
+    }
+
+    /**
+     * @return the current {@link Transaction} state.
+     */
+    TransactionState getState();
+
+    /**
+     * For a {@link Transaction} that has either been requested to discharge or has successfully
+     * discharged the {@link DischargeState} reflects whether the transaction was to be committed or
+     * rolled back.   Prior to a discharge being attempted there is no state value and this method
+     * returns {@link DischargeState.NONE}.
+     *
+     * @return the current {@link DischargeState} of the transaction.
+     */
+    DischargeState getDischargeState();
+
+    /**
+     * @return true if the {@link Transaction} has been marked declared by the {@link TransactionManager}.
+     */
+    boolean isDeclared();
+
+    /**
+     * @return true if the {@link Transaction} has been marked discharged by the {@link TransactionManager}.
+     */
+    boolean isDischarged();
+
+    /**
+     * The parent resource will mark the {@link Transaction} as failed is any of the operations performed on
+     * it cannot be successfully completed such as a {@link Declare} operation failing to write due to an IO
+     * error.
+     *
+     * @return true if the {@link Transaction} has been marked failed by the parent resource.
+     */
+    boolean isFailed();
+
+    /**
+     * If the declare or discharge of the transaction caused its state to become {@link TransactionState#FAILED}
+     * this method returns the {@link ErrorCondition} that the remote used to describe the reason for the failure.
+     *
+     * @return the {@link ErrorCondition} that the {@link TransactionManager} used to fail the {@link Transaction}.
+     */
+    ErrorCondition getCondition();
+
+    /**
+     * Returns a reference to the parent of this {@link Transaction} which will be either a
+     * {@link TransactionController} or a {@link TransactionManager} manager depending on the
+     * end of the {@link Link} that is operating on the {@link Transaction}.
+     *
+     * @return a reference to the parent of this {@link Transaction}.
+     */
+    E parent();
+
+    /**
+     * Returns the transaction Id that is associated with the declared transaction.  Prior to a
+     * {@link TransactionManager} completing a transaction declaration this method will return
+     * null to indicate that the transaction has not been declared yet.
+     *
+     * @return the transaction Id associated with the transaction once successfully declared.
+     */
+    Binary getTxnId();
+
+    /**
+     * @return the {@link Attachments} instance that is associated with this {@link Transaction}
+     */
+    Attachments getAttachments();
+
+    /**
+     * Links a given resource to this {@link Transaction}.
+     *
+     * @param resource
+     *      The resource to link to this {@link Transaction}.
+     */
+    void setLinkedResource(Object resource);
+
+    /**
+     * @return the user set linked resource for this {@link Transaction} instance.
+     */
+    Object getLinkedResource();
+
+    /**
+     * Gets the linked resource (if set) and returns it using the type information
+     * provided to cast the returned value.
+     *
+     * @param <T> The type to cast the linked resource to if one is set.
+     * @param typeClass the type's Class which is used for casting the returned value.
+     *
+     * @return the user set linked resource for this Context instance.
+     *
+     * @throws ClassCastException if the linked resource cannot be cast to the type requested.
+     */
+    <T> T getLinkedResource(Class<T> typeClass);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionController.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionController.java
new file mode 100644
index 0000000..9ee5435
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionController.java
@@ -0,0 +1,242 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import java.util.Collection;
+
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+
+/**
+ * Transaction Controller link that implements the mechanics of declaring and discharging
+ * AMQP transactions.  A {@link TransactionController} is typically used at the client side
+ * of an AMQP {@link Link} to create {@link Transaction} instances which the client application
+ * will enlist its incoming and outgoing deliveries into.
+ */
+public interface TransactionController extends Endpoint<TransactionController> {
+
+    /**
+     * Returns <code>true</code> if the {@link TransactionController} has capacity to send or buffer
+     * and {@link Transaction} command to {@link Declare} or {@link Discharge}.  If no capacity then
+     * a call to {@link TransactionController#declare()} or to
+     * {@link TransactionController#discharge(Transaction, boolean)} would throw an exception.
+     *
+     * @return true if the controller will allow declaring or discharging a transaction at this time.
+     */
+    boolean hasCapacity();
+
+    /**
+     * Sets the {@link Source} to assign to the local end of this {@link TransactionController}.
+     *
+     * Must be called during setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param source
+     *      The {@link Source} that will be set on the local end of this transaction controller.
+     *
+     * @return this transaction controller instance.
+     *
+     * @throws IllegalStateException if the {@link TransactionController} has already been opened.
+     */
+    TransactionController setSource(Source source) throws IllegalStateException;
+
+    /**
+     * @return the {@link Source} for the local end of this {@link TransactionController}.
+     */
+    Source getSource();
+
+    /**
+     * Sets the {@link Coordinator} target to assign to the local end of this {@link TransactionController}.
+     *
+     * Must be called during setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param coordinator
+     *      The {@link Coordinator} target that will be set on the local end of this transaction controller.
+     *
+     * @return this transaction controller instance.
+     *
+     * @throws IllegalStateException if the {@link TransactionController} has already been opened.
+     */
+    TransactionController setCoordinator(Coordinator coordinator) throws IllegalStateException;
+
+    /**
+     * Returns the currently set Coordinator target for this {@link Link}.
+     *
+     * @return the link target {@link Coordinator} for the local end of this link.
+     */
+    Coordinator getCoordinator();
+
+    /**
+     * @return the source {@link Source} for the remote end of this {@link TransactionController}.
+     */
+    Source getRemoteSource();
+
+    /**
+     * Returns the remote target {@link Terminus} for this transaction controller which must be of type
+     * {@link Coordinator} or null if remote did not set a terminus.
+     *
+     * @return the remote coordinator {@link Terminus} for the remote end of this link.
+     */
+    Coordinator getRemoteCoordinator();
+
+    /**
+     * Returns a list of {@link Transaction} objects that are active within this {@link TransactionController} which
+     * have not reached a terminal state meaning they have not been successfully discharged and have not failed in
+     * either the {@link Declare} phase or the {@link Discharge} phase.  If there are no transactions active within
+     * this {@link TransactionController} this method returns an empty {@link Collection}.
+     *
+     * @return a list of Transactions that are allocated to this controller that have not reached a terminal state.
+     */
+    Collection<Transaction<TransactionController>> transactions();
+
+    /**
+     * Creates a new {@link Transaction} instances that is returned in the {@link TransactionState#IDLE} state
+     * which can be populated with application specific attachments or assigned a linked resource prior to calling
+     * the
+     *
+     * @return a new {@link Transaction} instance that can be correlated with later declared events.
+     */
+    Transaction<TransactionController> newTransaction();
+
+    /**
+     * Request that the remote {@link TransactionManager} declare a new transaction and
+     * respond with a new transaction Id for that transaction.  Upon successful declaration of
+     * a new transaction the remote will respond and the {@link TransactionController#declaredHandler(EventHandler)}
+     * event handler will be signaled.
+     *
+     * This is a convenience method that is the same as first calling {@link TransactionController#newTransaction()}
+     * and then passing the result of that to the {@link TransactionController#declare(Transaction)} method.
+     *
+     * @return a new {@link Transaction} instance that can be correlated with later declared events.
+     */
+    Transaction<TransactionController> declare();
+
+    /**
+     * Request that the remote {@link TransactionManager} declare a new transaction and
+     * respond with a new transaction Id for that transaction.  Upon successful declaration of
+     * a new transaction the remote will respond and the {@link TransactionController#declaredHandler(EventHandler)}
+     * event handler will be signaled.
+     *
+     * @param transaction
+     *      The {@link Transaction} that is will be associated with the eventual declared transaction.
+     *
+     * @return this {@link TransactionController}
+     */
+    TransactionController declare(Transaction<TransactionController> transaction);
+
+    /**
+     * Request that the remote {@link TransactionManager} discharge the given transaction and
+     * with the specified failure state (true for failed).  Upon successful declaration of
+     * a new transaction the remote will respond and the {@link TransactionController#declaredHandler(EventHandler)}
+     * event handler will be signaled.
+     *
+     * @param transaction
+     *      The {@link Transaction} that is being discharged.
+     * @param failed
+     *      boolean value indicating the the discharge indicates the transaction failed (rolled back).
+     *
+     * @return this {@link TransactionController}
+     */
+    TransactionController discharge(Transaction<TransactionController> transaction, boolean failed);
+
+    /**
+     * Called when the {@link TransactionManager} end of the link has responded to a previous
+     * {@link Declare} request and the transaction can now be used to enroll deliveries into the
+     * active transaction.
+     *
+     * @param declaredEventHandler
+     *      An {@link EventHandler} that will act on the transaction declaration request.
+     *
+     * @return this {@link TransactionController}.
+     */
+    TransactionController declaredHandler(EventHandler<Transaction<TransactionController>> declaredEventHandler);
+
+    /**
+     * Called when the {@link TransactionManager} end of the link responds to a {@link Transaction} declaration
+     * with an {@link Rejected} outcome indicating that the transaction could not be successfully declared.
+     *
+     * @param declareFailureEventHandler
+     *      An {@link EventHandler} that will be called when a previous transaction declaration fails.
+     *
+     * @return this {@link TransactionController}.
+     */
+    TransactionController declareFailureHandler(EventHandler<Transaction<TransactionController>> declareFailureEventHandler);
+
+    /**
+     * Called when the {@link TransactionManager} end of the link has responded to a previous
+     * {@link TransactionController#discharge(Transaction, boolean)} request and the transaction has
+     * been retired.
+     *
+     * @param dischargedEventHandler
+     *      An {@link EventHandler} that will act on the transaction discharge request.
+     *
+     * @return this {@link TransactionController}.
+     */
+    TransactionController dischargedHandler(EventHandler<Transaction<TransactionController>> dischargedEventHandler);
+
+    /**
+     * Called when the {@link TransactionManager} end of the link has responded to a previous
+     * {@link TransactionController#discharge(Transaction, boolean)} request and the transaction discharge
+     * failed for some reason.
+     *
+     * @param dischargeFailureEventHandler
+     *      An {@link EventHandler} that will act on the transaction discharge failed event.
+     *
+     * @return this {@link TransactionController}.
+     */
+    TransactionController dischargeFailureHandler(EventHandler<Transaction<TransactionController>> dischargeFailureEventHandler);
+
+    /**
+     * Allows the caller to add an {@link EventHandler} that will be signaled when the underlying
+     * link for this {@link TransactionController} has been granted credit which would then allow for
+     * transaction {@link Declared} and {@link Discharge} commands to be sent to the remote Transactional
+     * Resource.
+     *
+     * If the controller already has credit to send then the handler will be invoked immediately otherwise
+     * it will be stored until credit becomes available.  Once a handler is signaled it is no longer retained
+     * for future updates and the caller will need to register it again once more transactional work is to be
+     * completed.  Because more than one handler can be added at a time the caller should check again before
+     * attempting to perform a transaction {@link Declared} or {@link Discharge} is performed as other tasks
+     * might have already consumed credit if work is done via some asynchronous mechanism.
+     *
+     * @param handler
+     *      The {@link EventHandler} that will be signaled once credit is available for transaction work.
+     *
+     * @return this {@link TransactionController} instance.
+     */
+    TransactionController addCapacityAvailableHandler(EventHandler<TransactionController> handler);
+
+    /**
+     * Sets a {@link EventHandler} for when the parent {@link Session} or {@link Connection} of this {@link TransactionController}
+     * is locally closed.
+     *
+     * Typically used by clients for logging or other state update event processing.  Clients should not perform any
+     * blocking calls within this context.  It is an error for the handler to throw an exception and the outcome of
+     * doing so is undefined.
+     *
+     * @param handler
+     *      The {@link EventHandler} to notify when this transaction controller's parent endpoint is locally closed.
+     *
+     * @return the link for chaining.
+     */
+    TransactionController parentEndpointClosedHandler(EventHandler<TransactionController> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionManager.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionManager.java
new file mode 100644
index 0000000..29626ff
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionManager.java
@@ -0,0 +1,218 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Transaction Manager endpoint that implements the mechanics of handling the declaration
+ * of and the requested discharge of AMQP transactions.  Typically an AMQP server instance
+ * will host the transaction management services that are used by client resources to declare
+ * and discharge transaction and handle the associated of deliveries that are enlisted in
+ * active transactions.
+ */
+public interface TransactionManager extends Endpoint<TransactionManager> {
+
+    /**
+     * Adds the given amount of credit for the {@link TransactionManager} which allows
+     * the {@link TransactionController} to send {@link Declare} and {@link Discharge}
+     * requests to this manager.  The {@link TransactionController} cannot send any requests
+     * to start or complete a transaction without having credit to do so which implies that
+     * the {@link TransactionManager} owner must grant credit as part of its normal processing.
+     *
+     * @param additionalCredit
+     *      the new amount of credits to add.
+     *
+     * @return this {@link TransactionManager}
+     *
+     * @throws IllegalArgumentException if the credit amount is negative.
+     */
+    TransactionManager addCredit(int additionalCredit);
+
+    /**
+     * Get the credit that is currently available or assigned to this {@link TransactionManager}.
+     *
+     * @return the current unused credit.
+     */
+    int getCredit();
+
+    /**
+     * Sets the {@link Source} to assign to the local end of this {@link TransactionManager}.
+     *
+     * Must be called during setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param source
+     *      The {@link Source} that will be set on the local end of this transaction controller.
+     *
+     * @return this transaction controller instance.
+     *
+     * @throws IllegalStateException if the {@link TransactionManager} has already been opened.
+     */
+    TransactionManager setSource(Source source) throws IllegalStateException;
+
+    /**
+     * @return the {@link Source} for the local end of this {@link TransactionController}.
+     */
+    Source getSource();
+
+    /**
+     * Sets the {@link Coordinator} target to assign to the local end of this {@link TransactionManager}.
+     *
+     * Must be called during setup, i.e. before calling the {@link #open()} method.
+     *
+     * @param coordinator
+     *      The {@link Coordinator} target that will be set on the local end of this transaction controller.
+     *
+     * @return this transaction controller instance.
+     *
+     * @throws IllegalStateException if the {@link TransactionManager} has already been opened.
+     */
+    TransactionManager setCoordinator(Coordinator coordinator) throws IllegalStateException;
+
+    /**
+     * Returns the currently set Coordinator target for this {@link Link}.
+     *
+     * @return the link target {@link Coordinator} for the local end of this link.
+     */
+    Coordinator getCoordinator();
+
+    /**
+     * @return the source {@link Source} for the remote end of this {@link TransactionManager}.
+     */
+    Source getRemoteSource();
+
+    /**
+     * Returns the remote target {@link Terminus} for this transaction manager which must be of type
+     * {@link Coordinator} or null if remote did not set a terminus.
+     *
+     * @return the remote coordinator {@link Terminus} for the remote end of this link.
+     */
+    Coordinator getRemoteCoordinator();
+
+    /**
+     * Respond to a previous {@link Declare} request from the remote {@link TransactionController}
+     * indicating that the requested transaction has been successfully declared and that deliveries
+     * can now be enlisted in that transaction.
+     *
+     * @param transaction
+     *      The transaction instance that is associated with the declared transaction.
+     * @param txnId
+     *      The binary transaction Id to assign the now declared transaction instance.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    default TransactionManager declared(Transaction<TransactionManager> transaction, byte[] txnId) {
+        return declared(transaction, new Binary(txnId));
+    }
+
+    /**
+     * Respond to a previous {@link Declare} request from the remote {@link TransactionController}
+     * indicating that the requested transaction has been successfully declared and that deliveries
+     * can now be enlisted in that transaction.
+     *
+     * @param transaction
+     *      The transaction instance that is associated with the declared transaction.
+     * @param txnId
+     *      The binary transaction Id to assign the now declared transaction instance.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    TransactionManager declared(Transaction<TransactionManager> transaction, Binary txnId);
+
+    /**
+     * Respond to a previous {@link Declare} request from the remote {@link TransactionController}
+     * indicating that the requested transaction declaration has failed and is not active.
+     *
+     * @param transaction
+     *      The transaction instance that is associated with the declared transaction.
+     * @param condition
+     *      The {@link ErrorCondition} that described the reason for the transaction failure.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    TransactionManager declareFailed(Transaction<TransactionManager> transaction, ErrorCondition condition);
+
+    /**
+     * Respond to a previous {@link Discharge} request from the remote {@link TransactionController}
+     * indicating that the discharge completed on the transaction identified by given transaction Id
+     * has now been retired.
+     *
+     * @param transaction
+     *      The {@link Transaction} instance that has been discharged and is now retired.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    TransactionManager discharged(Transaction<TransactionManager> transaction);
+
+    /**
+     * Respond to a previous {@link Discharge} request from the remote {@link TransactionController}
+     * indicating that the discharge resulted in an error and the transaction must be considered rolled
+     * back.
+     *
+     * @param transaction
+     *      The {@link Transaction} instance that has been discharged and is now retired.
+     * @param condition
+     *      The {@link ErrorCondition} that described the reason for the transaction failure.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    TransactionManager dischargeFailed(Transaction<TransactionManager> transaction, ErrorCondition condition);
+
+    /**
+     * Called when the {@link TransactionController} end of the link has requested a new transaction be
+     * declared using the information provided in the given {@link Declare} instance.
+     *
+     * @param declaredEventHandler
+     *      handler that will act on the transaction declaration request.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    TransactionManager declareHandler(EventHandler<Transaction<TransactionManager>> declaredEventHandler);
+
+    /**
+     * Called when the {@link TransactionController} end of the link has requested a current transaction be
+     * discharged using the information provided in the given {@link Discharge} instance.
+     *
+     * @param dischargeEventHandler
+     *      handler that will act on the transaction declaration request.
+     *
+     * @return this {@link TransactionManager}.
+     */
+    TransactionManager dischargeHandler(EventHandler<Transaction<TransactionManager>> dischargeEventHandler);
+
+    /**
+     * Sets a {@link EventHandler} for when the parent {@link Session} or {@link Connection} of this {@link TransactionManager}
+     * is locally closed.
+     *
+     * Typically used by clients for logging or other state update event processing.  Clients should not perform any
+     * blocking calls within this context.  It is an error for the handler to throw an exception and the outcome of
+     * doing so is undefined.
+     *
+     * @param handler
+     *      The {@link EventHandler} to notify when this transaction manger's parent endpoint is locally closed.
+     *
+     * @return the link for chaining.
+     */
+    TransactionManager parentEndpointClosedHandler(EventHandler<TransactionManager> handler);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionState.java
new file mode 100644
index 0000000..9100f45
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/TransactionState.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+
+/**
+ * Indicates the current state of a given {@link Transaction}
+ */
+public enum TransactionState {
+
+    /**
+     * A {@link Transaction} is considered IDLE until the {@link TransactionManager} responds that
+     * it has been declared successfully and an transaction Id has been assigned.
+     */
+    IDLE,
+
+    /**
+     * A {@link Transaction} is considered declaring once a Declare command has been sent to the remote
+     * but before any response has been received which assigns the transaction ID.
+     */
+    DECLARING,
+
+    /**
+     * A {@link Transaction} is considered declared once the {@link TransactionManager} has responded
+     * in the affirmative and assigned a transaction Id.
+     */
+    DECLARED,
+
+    /**
+     * A {@link Transaction} is considered to b discharging once a Discharge command has been sent to the remote
+     * but before any response has been received indicating the outcome of the attempted discharge.
+     */
+    DISCHARGING,
+
+    /**
+     * A {@link Transaction} is considered discharged once a {@link Discharge} has been requested and
+     * the {@link TransactionManager} has responded in the affirmative that the request has been honored.
+     */
+    DISCHARGED,
+
+    /**
+     * A {@link Transaction} is considered failed in the {@link TransactionManager} responds with an error
+     * to the {@link Declare} action.
+     */
+    DECLARE_FAILED,
+
+    /**
+     * A {@link Transaction} is considered failed in the {@link TransactionManager} responds with an error
+     * to the {@link Discharge} action.
+     */
+    DISCHARGE_FAILED;
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineFailedException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineFailedException.java
new file mode 100644
index 0000000..5c75bb5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineFailedException.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Thrown from Engine API methods that attempted an operation what would have
+ * resulted in a write of data or other state modification after the engine has
+ * entered the the failed state.
+ */
+public final class EngineFailedException extends EngineStateException {
+
+    private static final long serialVersionUID = 5947522999263302647L;
+
+    public EngineFailedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EngineFailedException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Allows for duplication of {@link EngineFailedException} exceptions which
+     * preserve the message and original cause.
+     *
+     * @return new {@link EngineFailedException} which preserves details from the original.
+     */
+    EngineFailedException duplicate() {
+        return new EngineFailedException(getMessage(), getCause());
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineNotStartedException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineNotStartedException.java
new file mode 100644
index 0000000..a433e9f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineNotStartedException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Thrown when a read or write operation is attempted on the engine before
+ * it has been properly started.
+ */
+public final class EngineNotStartedException extends EngineStateException {
+
+    private static final long serialVersionUID = -4545732230266096598L;
+
+    public EngineNotStartedException() {
+    }
+
+    public EngineNotStartedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EngineNotStartedException(String message) {
+        super(message);
+    }
+
+    public EngineNotStartedException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineNotWritableException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineNotWritableException.java
new file mode 100644
index 0000000..d7bdc92
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineNotWritableException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Exception indicating that the engine is not currently accepting input of data
+ */
+public final class EngineNotWritableException extends EngineStateException {
+
+    private static final long serialVersionUID = 4395349183049727897L;
+
+    public EngineNotWritableException() {
+        super();
+    }
+
+    public EngineNotWritableException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EngineNotWritableException(String message) {
+        super(message);
+    }
+
+    public EngineNotWritableException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineShutdownException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineShutdownException.java
new file mode 100644
index 0000000..fc6a45a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineShutdownException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Exception thrown when an option is performed on a closed engine.
+ */
+public final class EngineShutdownException extends EngineStateException {
+
+    private static final long serialVersionUID = 7020379252988873878L;
+
+    public EngineShutdownException() {
+    }
+
+    public EngineShutdownException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EngineShutdownException(String message) {
+        super(message);
+    }
+
+    public EngineShutdownException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineStartedException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineStartedException.java
new file mode 100644
index 0000000..945575b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineStartedException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Thrown when an API method has been called which cannot be allowed to proceed
+ * due to the engine having already been started and doesn't allow modification to
+ * the resource in question after that point.
+ */
+public class EngineStartedException extends EngineStateException {
+
+    private static final long serialVersionUID = -2619256904685368538L;
+
+    public EngineStartedException(String message) {
+        super(message);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineStateException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineStateException.java
new file mode 100644
index 0000000..8f67026
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/EngineStateException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Root type for exceptions thrown from the engine due to state violations
+ */
+public abstract class EngineStateException extends ProtonException {
+
+    private static final long serialVersionUID = 4191691747006604768L;
+
+    public EngineStateException() {
+    }
+
+    public EngineStateException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EngineStateException(String message) {
+        super(message);
+    }
+
+    public EngineStateException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/FrameDecodingException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/FrameDecodingException.java
new file mode 100644
index 0000000..0476de9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/FrameDecodingException.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+
+/**
+ * Exception thrown when the engine cannot decode an incoming frame due to some
+ * error either with the encoding itself or the contents which cause a specification
+ * violation.
+ */
+public class FrameDecodingException extends ProtocolViolationException {
+
+    private static final long serialVersionUID = -1226121804157774724L;
+
+    public FrameDecodingException() {
+        super(AmqpError.DECODE_ERROR);
+    }
+
+    public FrameDecodingException(String message, Throwable cause) {
+        super(AmqpError.DECODE_ERROR, message, cause);
+    }
+
+    public FrameDecodingException(String message) {
+        super(AmqpError.DECODE_ERROR, message);
+    }
+
+    public FrameDecodingException(Throwable cause) {
+        super(AmqpError.DECODE_ERROR, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/FrameEncodingException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/FrameEncodingException.java
new file mode 100644
index 0000000..933bc5e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/FrameEncodingException.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+
+/**
+ * Exception thrown when the engine cannot encode a frame from a given performative
+ * and or payload combination.
+ */
+public class FrameEncodingException extends ProtocolViolationException {
+
+    private static final long serialVersionUID = -5392939106677054003L;
+
+    public FrameEncodingException() {
+        super(AmqpError.INTERNAL_ERROR);
+    }
+
+    public FrameEncodingException(String message, Throwable cause) {
+        super(AmqpError.INTERNAL_ERROR, message, cause);
+    }
+
+    public FrameEncodingException(String message) {
+        super(AmqpError.INTERNAL_ERROR, message);
+    }
+
+    public FrameEncodingException(Throwable cause) {
+        super(AmqpError.INTERNAL_ERROR, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/IdleTimeoutException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/IdleTimeoutException.java
new file mode 100644
index 0000000..e08d928
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/IdleTimeoutException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Error thrown when the Engine idle checking detects a timeout condition and
+ * shuts down the engine and places it in an error state.
+ */
+public class IdleTimeoutException extends ProtonException {
+
+    private static final long serialVersionUID = 6527918786644498627L;
+
+    public IdleTimeoutException() {
+    }
+
+    public IdleTimeoutException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public IdleTimeoutException(String message) {
+        super(message);
+    }
+
+    public IdleTimeoutException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/MalformedAMQPHeaderException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/MalformedAMQPHeaderException.java
new file mode 100644
index 0000000..ff1dd42
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/MalformedAMQPHeaderException.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+
+/**
+ * Exception thrown when an incoming AMQP Header response does not conform to the
+ * AMQP Header specification.
+ */
+public class MalformedAMQPHeaderException extends ProtocolViolationException {
+
+    private static final long serialVersionUID = 6679970155102489530L;
+
+    public MalformedAMQPHeaderException() {
+        super(AmqpError.DECODE_ERROR);
+    }
+
+    public MalformedAMQPHeaderException(String message, Throwable cause) {
+        super(AmqpError.DECODE_ERROR, message, cause);
+    }
+
+    public MalformedAMQPHeaderException(String message) {
+        super(AmqpError.DECODE_ERROR, message);
+    }
+
+    public MalformedAMQPHeaderException(Throwable cause) {
+        super(AmqpError.DECODE_ERROR, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtocolViolationException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtocolViolationException.java
new file mode 100644
index 0000000..8830964
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtocolViolationException.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Error thrown when there has been a violation of the AMQP specification
+ */
+public class ProtocolViolationException extends ProtonException {
+
+    private static final long serialVersionUID = 1L;
+
+    private Symbol condition;
+
+    public ProtocolViolationException() {
+        super();
+    }
+
+    public ProtocolViolationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ProtocolViolationException(String message) {
+        super(message);
+    }
+
+    public ProtocolViolationException(Throwable cause) {
+        super(cause);
+    }
+
+    public ProtocolViolationException(Symbol condition) {
+        super();
+
+        this.condition = condition;
+    }
+
+    public ProtocolViolationException(Symbol condition, String message, Throwable cause) {
+        super(message, cause);
+
+        this.condition = condition;
+    }
+
+    public ProtocolViolationException(Symbol condition, String message) {
+        super(message);
+
+        this.condition = condition;
+    }
+
+    public ProtocolViolationException(Symbol condition, Throwable cause) {
+        super(cause);
+
+        this.condition = condition;
+    }
+
+    public Symbol getErrorCondition() {
+        return condition;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonException.java
new file mode 100644
index 0000000..24f9123
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonException.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Base Proton Exception type that backs all the various exceptions that
+ * are thrown from within the Proton Engine.
+ */
+public class ProtonException extends RuntimeException {
+
+    private static final long serialVersionUID = -8458098856099248658L;
+
+    public ProtonException() {
+        super();
+    }
+
+    public ProtonException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ProtonException(String message) {
+        super(message);
+    }
+
+    public ProtonException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonExceptionSupport.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonExceptionSupport.java
new file mode 100644
index 0000000..9b08e39
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonExceptionSupport.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+public class ProtonExceptionSupport {
+
+    /**
+     * Checks the given cause to determine if it's already an ProtonException type and
+     * if not creates a new ProtonException to wrap it.
+     *
+     * @param cause
+     *        The initiating exception that should be cast or wrapped.
+     *
+     * @return an ProtonException instance.
+     */
+    public static EngineFailedException createFailedException(Throwable cause) {
+        return createFailedException(null, cause);
+    }
+
+    /**
+     * Creates a new instance of an EngineFailedException that either wraps the given cause
+     * if it is not an instance of an {@link EngineFailedException} or creates a new copy
+     * of the given {@link EngineFailedException} in order to produce a meaningful stack trace
+     * for the caller of which of their calls failed due to the engine state being failed.
+     *
+     * @param message
+     *        A descriptive message to be applied to the returned exception.
+     * @param cause
+     *        The initiating exception that should be cast or wrapped.
+     *
+     * @return an ProtonException instance.
+     */
+    public static EngineFailedException createFailedException(String message, Throwable cause) {
+        if (cause instanceof EngineFailedException) {
+            return ((EngineFailedException) cause).duplicate();
+        }
+
+        if (cause.getCause() instanceof EngineFailedException) {
+            return ((EngineFailedException) cause.getCause()).duplicate();
+        }
+
+        if (message == null || message.isEmpty()) {
+            message = cause.getMessage();
+            if (message == null || message.length() == 0) {
+                message = cause.toString();
+            }
+        }
+
+        return new EngineFailedException(message, cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonIOException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonIOException.java
new file mode 100644
index 0000000..b0fab0d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/exceptions/ProtonIOException.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.exceptions;
+
+/**
+ * Thrown when an unchecked IO exception is encountered from a write event that
+ * invokes an outside write handler from the engine.
+ */
+public class ProtonIOException extends ProtonException {
+
+    private static final long serialVersionUID = -8458098856099248658L;
+
+    public ProtonIOException() {
+        super();
+    }
+
+    public ProtonIOException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ProtonIOException(String message) {
+        super(message);
+    }
+
+    public ProtonIOException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonAttachments.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonAttachments.java
new file mode 100644
index 0000000..3cbece2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonAttachments.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.engine.Attachments;
+
+/**
+ * Proton implementation of an Attachments object.
+ */
+public class ProtonAttachments implements Attachments {
+
+    private final Map<Object, Object> contextMap = new HashMap<>();
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T get(String key) {
+        return contextMap == null ? null : (T) contextMap.get(key);
+    }
+
+    @Override
+    public <T> T get(String key, Class<T> typeClass) {
+        return typeClass.cast(contextMap.get(key));
+    }
+
+    @Override
+    public ProtonAttachments set(String key, Object value) {
+        contextMap.put(key, value);
+        return this;
+    }
+
+    @Override
+    public boolean containsKey(String key) {
+        return contextMap.containsKey(key);
+    }
+
+    @Override
+    public Attachments clear() {
+        if (contextMap != null) {
+            contextMap.clear();
+        }
+
+        return this;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonConnection.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonConnection.java
new file mode 100644
index 0000000..089046b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonConnection.java
@@ -0,0 +1,760 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.lang.ref.SoftReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.ConnectionState;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.SessionState;
+import org.apache.qpid.protonj2.engine.TransactionManager;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.apache.qpid.protonj2.types.transport.Performative;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Implements the proton Connection API
+ */
+public class ProtonConnection extends ProtonEndpoint<Connection> implements Connection, AMQPHeader.HeaderHandler<ProtonEngine>, Performative.PerformativeHandler<ProtonEngine> {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonConnection.class);
+
+    private final Open localOpen = new Open();
+    private Open remoteOpen;
+    private AMQPHeader remoteHeader;
+
+    private Map<Integer, ProtonSession> localSessions = new HashMap<>();
+    private Map<Integer, ProtonSession> remoteSessions = new HashMap<>();
+
+    // These would be sessions that were begun and ended before the remote ever
+    // responded with a matching being and end.  The remote is required to complete
+    // these before answering a new begin sequence on the same local channel.
+    private Map<Integer, SoftReference<ProtonSession>> zombieSessions = new LinkedHashMap<>();
+
+    private ConnectionState localState = ConnectionState.IDLE;
+    private ConnectionState remoteState = ConnectionState.IDLE;
+
+    private boolean headerSent;
+    private boolean localOpenSent;
+    private boolean localCloseSent;
+
+    private EventHandler<AMQPHeader> remoteHeaderHandler;
+    private EventHandler<Session> remoteSessionOpenEventHandler;
+    private EventHandler<Sender> remoteSenderOpenEventHandler;
+    private EventHandler<Receiver> remoteReceiverOpenEventHandler;
+    private EventHandler<TransactionManager> remoteTxnManagerOpenEventHandler;
+
+    /**
+     * Create a new unbound Connection instance.
+     *
+     * @param engine
+     */
+    ProtonConnection(ProtonEngine engine) {
+        super(engine);
+
+        // This configures the default for the client which could later be made configurable
+        // by adding an option in EngineConfiguration but for now this is forced set here.
+        this.localOpen.setMaxFrameSize(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE);
+    }
+
+    @Override
+    public Connection getParent() {
+        return this;
+    }
+
+    @Override
+    ProtonConnection self() {
+        return this;
+    }
+
+    @Override
+    public ConnectionState getState() {
+        return localState;
+    }
+
+    @Override
+    public ProtonConnection open() throws EngineStateException {
+        if (getState() == ConnectionState.IDLE) {
+            engine.checkShutdownOrFailed("Cannot open a connection when Engine is shutdown or failed.");
+            localState = ConnectionState.ACTIVE;
+            try {
+                syncLocalStateWithRemote();
+            } finally {
+                fireLocalOpen();
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonConnection close() throws EngineFailedException {
+        if (getState() == ConnectionState.ACTIVE) {
+            localState = ConnectionState.CLOSED;
+            try {
+                getEngine().checkFailed("Connection close called while engine .");
+                syncLocalStateWithRemote();
+            } finally {
+                allSessions().forEach(session -> session.handleConnectionLocallyClosed(this));
+                fireLocalClose();
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public Connection negotiate() {
+        return negotiate((header) -> {
+            LOG.trace("Negotiation completed with remote returning AMQP Header: {}", header);
+        });
+    }
+
+    @Override
+    public Connection negotiate(EventHandler<AMQPHeader> remoteAMQPHeaderHandler) {
+        Objects.requireNonNull(remoteAMQPHeaderHandler, "Provided AMQP Header received handler cannot be null");
+        checkConnectionClosed("Cannot start header negotiation on a closed connection");
+
+        if (remoteHeader != null) {
+            remoteAMQPHeaderHandler.handle(remoteHeader);
+        } else {
+            remoteHeaderHandler = remoteAMQPHeaderHandler;
+        }
+
+        syncLocalStateWithRemote();
+
+        return this;
+    }
+
+    @Override
+    public long tick(long current) {
+        checkConnectionClosed("Cannot call tick on an already closed Connection");
+        return engine.tick(current);
+    }
+
+    @Override
+    public Connection tickAuto(ScheduledExecutorService executor) {
+        checkConnectionClosed("Cannot call tickAuto on an already closed Connection");
+        engine.tickAuto(executor);
+        return this;
+    }
+
+    @Override
+    public boolean isLocallyClosed() {
+        return getState() == ConnectionState.CLOSED;
+    }
+
+    @Override
+    public boolean isRemotelyClosed() {
+        return getRemoteState() == ConnectionState.CLOSED;
+    }
+
+    @Override
+    public ProtonConnection setContainerId(String containerId) {
+        checkNotOpened("Cannot set Container Id on already opened Connection");
+        localOpen.setContainerId(containerId);
+        return this;
+    }
+
+    @Override
+    public String getContainerId() {
+        return localOpen.getContainerId();
+    }
+
+    @Override
+    public ProtonConnection setHostname(String hostname) {
+        checkNotOpened("Cannot set Hostname on already opened Connection");
+        localOpen.setHostname(hostname);
+        return this;
+    }
+
+    @Override
+    public String getHostname() {
+        return localOpen.getHostname();
+    }
+
+    @Override
+    public Connection setMaxFrameSize(long maxFrameSize) {
+        checkNotOpened("Cannot set Max Frame Size on already opened Connection");
+
+        // We are specifically limiting max frame size to 2GB here as our buffers implementations
+        // cannot handle anything larger so we must protect them from larger frames.
+        if (maxFrameSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException(String.format(
+                "Given max frame size value %d larger than this implementations limit of %d",
+                maxFrameSize, Integer.MAX_VALUE));
+        }
+
+        localOpen.setMaxFrameSize(maxFrameSize);
+        return this;
+    }
+
+    @Override
+    public long getMaxFrameSize() {
+        return localOpen.getMaxFrameSize();
+    }
+
+    @Override
+    public ProtonConnection setChannelMax(int channelMax) {
+        checkNotOpened("Cannot set Channel Max on already opened Connection");
+        localOpen.setChannelMax(channelMax);
+        return this;
+    }
+
+    @Override
+    public int getChannelMax() {
+        return localOpen.getChannelMax();
+    }
+
+    @Override
+    public ProtonConnection setIdleTimeout(long idleTimeout) {
+        checkNotOpened("Cannot set Idle Timeout on already opened Connection");
+        localOpen.setIdleTimeout(idleTimeout);
+        return this;
+    }
+
+    @Override
+    public long getIdleTimeout() {
+        return localOpen.getIdleTimeout();
+    }
+
+    @Override
+    public ProtonConnection setOfferedCapabilities(Symbol... capabilities) {
+        checkNotOpened("Cannot set Offered Capabilities on already opened Connection");
+
+        if (capabilities != null) {
+            localOpen.setOfferedCapabilities(Arrays.copyOf(capabilities, capabilities.length));
+        } else {
+            localOpen.setOfferedCapabilities(capabilities);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Symbol[] getOfferedCapabilities() {
+        if (localOpen.getOfferedCapabilities() != null) {
+            return Arrays.copyOf(localOpen.getOfferedCapabilities(), localOpen.getOfferedCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public ProtonConnection setDesiredCapabilities(Symbol... capabilities) {
+        checkNotOpened("Cannot set Desired Capabilities on already opened Connection");
+
+        if (capabilities != null) {
+            localOpen.setDesiredCapabilities(Arrays.copyOf(capabilities, capabilities.length));
+        } else {
+            localOpen.setDesiredCapabilities(capabilities);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Symbol[] getDesiredCapabilities() {
+        if (localOpen.getDesiredCapabilities() != null) {
+            return Arrays.copyOf(localOpen.getDesiredCapabilities(), localOpen.getDesiredCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public ProtonConnection setProperties(Map<Symbol, Object> properties) {
+        checkNotOpened("Cannot set Properties on already opened Connection");
+
+        if (properties != null) {
+            localOpen.setProperties(new LinkedHashMap<>(properties));
+        } else {
+            localOpen.setProperties(properties);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Map<Symbol, Object> getProperties() {
+        if (localOpen.getProperties() != null) {
+            return Collections.unmodifiableMap(localOpen.getProperties());
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean isLocallyOpen() {
+        return getState() == ConnectionState.ACTIVE;
+    }
+
+    @Override
+    public boolean isRemotelyOpen() {
+        return getRemoteState() == ConnectionState.ACTIVE;
+    }
+
+    @Override
+    public String getRemoteContainerId() {
+        return remoteOpen == null ? null : remoteOpen.getContainerId();
+    }
+
+    @Override
+    public String getRemoteHostname() {
+        return remoteOpen == null ? null : remoteOpen.getHostname();
+    }
+
+    @Override
+    public long getRemoteMaxFrameSize() {
+        return remoteOpen == null ? ProtonConstants.MIN_MAX_AMQP_FRAME_SIZE : remoteOpen.getMaxFrameSize();
+    }
+
+    @Override
+    public long getRemoteIdleTimeout() {
+        return remoteOpen == null ? -1 : remoteOpen.getIdleTimeout();
+    }
+
+    @Override
+    public Symbol[] getRemoteOfferedCapabilities() {
+        if (remoteOpen != null && remoteOpen.getOfferedCapabilities() != null) {
+            return Arrays.copyOf(remoteOpen.getOfferedCapabilities(), remoteOpen.getOfferedCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Symbol[] getRemoteDesiredCapabilities() {
+        if (remoteOpen != null && remoteOpen.getDesiredCapabilities() != null) {
+            return Arrays.copyOf(remoteOpen.getDesiredCapabilities(), remoteOpen.getDesiredCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Map<Symbol, Object> getRemoteProperties() {
+        if (remoteOpen != null && remoteOpen.getProperties() != null) {
+            return Collections.unmodifiableMap(remoteOpen.getProperties());
+        }
+
+        return null;
+    }
+
+    @Override
+    public ConnectionState getRemoteState() {
+        return remoteState;
+    }
+
+    @Override
+    public ProtonSession session() throws IllegalStateException {
+        checkConnectionClosed("Cannot create a Session from a Connection that is already closed");
+
+        int localChannel = findFreeLocalChannel();
+        ProtonSession newSession = new ProtonSession(this, localChannel);
+        localSessions.put(localChannel, newSession);
+
+        return newSession;
+    }
+
+    @Override
+    public Set<Session> sessions() throws IllegalStateException {
+        return Collections.unmodifiableSet(allSessions());
+    }
+
+    //----- Handle performatives sent from the remote to this Connection
+
+    @Override
+    public void handleAMQPHeader(AMQPHeader header, ProtonEngine context) {
+        remoteHeader = header;
+
+        if (remoteHeaderHandler != null) {
+            remoteHeaderHandler.handle(remoteHeader);
+            remoteHeaderHandler = null;
+        }
+
+        syncLocalStateWithRemote();
+    }
+
+    @Override
+    public void handleSASLHeader(AMQPHeader header, ProtonEngine context) {
+        context.engineFailed(new ProtocolViolationException("Receivded unexpected SASL Header"));
+    }
+
+    @Override
+    public void handleOpen(Open open, ProtonBuffer payload, int channel, ProtonEngine context) {
+        if (remoteOpen != null) {
+            context.engineFailed(new ProtocolViolationException("Received second Open for Connection from remote"));
+            return;
+        }
+
+        remoteState = ConnectionState.ACTIVE;
+        remoteOpen = open;
+
+        fireRemoteOpen();
+    }
+
+    @Override
+    public void handleClose(Close close, ProtonBuffer payload, int channel, ProtonEngine context) {
+        remoteState = ConnectionState.CLOSED;
+        setRemoteCondition(close.getError());
+        allSessions().forEach(session -> session.handleConnectionRemotelyClosed(this));
+
+        fireRemoteClose();
+    }
+
+    @Override
+    public void handleBegin(Begin begin, ProtonBuffer payload, int channel, ProtonEngine context) {
+        ProtonSession session = null;
+
+        if (channel > localOpen.getChannelMax()) {
+            setCondition(new ErrorCondition(ConnectionError.FRAMING_ERROR, "Channel Max Exceeded for session Begin")).close();
+        } else if (remoteSessions.containsKey(channel)) {
+            context.engineFailed(new ProtocolViolationException("Received second begin for Session from remote"));
+        } else {
+            // If there is a remote channel then this is an answer to a local open of a session, otherwise
+            // the remote is requesting a new session and we need to create one and signal that a remote
+            // session was opened.
+            if (begin.hasRemoteChannel()) {
+                final int localSessionChannel = begin.getRemoteChannel();
+                session = localSessions.get(localSessionChannel);
+                if (session == null) {
+                    // If there is a session that was begun and ended before remote responded we
+                    // expect that this exchange refers to that session and proceed as though the
+                    // remote is going to begin and end it now (as it should).  The alternative is
+                    // that the remote is doing something not compliant with the specification and
+                    // we fail the engine to indicate this.
+                    if (zombieSessions.containsKey(localSessionChannel)) {
+                        session = zombieSessions.get(localSessionChannel).get();
+                        if (session != null) {
+                            // The session will now get tracked as a remote session and the next
+                            // end will take care of normal remote session cleanup.
+                            zombieSessions.remove(localSessionChannel);
+                        } else {
+                            // The session was reclaimed by GC and we retain the fact that it was
+                            // here so that the end that should be following doesn't result in an
+                            // engine failure.
+                            return;
+                        }
+                    } else {
+                        setCondition(new ErrorCondition(AmqpError.PRECONDITION_FAILED, "No matching session found for remote channel given")).close();
+                        engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on Begin from remote: " + localSessionChannel));
+                        return;
+                    }
+                }
+            } else {
+                session = session();
+            }
+
+            remoteSessions.put(channel, session);
+
+            // Let the session handle the remote Begin now.
+            session.remoteBegin(begin, channel);
+
+            // If the session was initiated remotely then we signal the creation to the any registered
+            // remote session event handler
+            if (session.getState() == SessionState.IDLE && remoteSessionOpenEventHandler != null) {
+                remoteSessionOpenEventHandler.handle(session);
+            }
+        }
+    }
+
+    @Override
+    public void handleEnd(End end, ProtonBuffer payload, int channel, ProtonEngine context) {
+        final ProtonSession session = remoteSessions.remove(channel);
+        if (session == null) {
+            // Check that we don't have a lingering session that was opened and closed locally for
+            // which the remote is finally getting round to ending but we lost the session instance
+            // due to it being cleaned up by GC,
+            if (zombieSessions.remove(channel) == null) {
+                engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on End from remote: " + channel));
+            }
+        } else {
+            session.remoteEnd(end, channel);
+        }
+    }
+
+    @Override
+    public void handleAttach(Attach attach, ProtonBuffer payload, int channel, ProtonEngine context) {
+        final ProtonSession session = remoteSessions.get(channel);
+        if (session == null) {
+            engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on Attach from remote: " + channel));
+        } else {
+            session.remoteAttach(attach, channel);
+        }
+    }
+
+    @Override
+    public void handleDetach(Detach detach, ProtonBuffer payload, int channel, ProtonEngine context) {
+        final ProtonSession session = remoteSessions.get(channel);
+        if (session == null) {
+            engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on Detach from remote: " + channel));
+        } else {
+            session.remoteDetach(detach, channel);
+        }
+    }
+
+    @Override
+    public void handleFlow(Flow flow, ProtonBuffer payload, int channel, ProtonEngine context) {
+        final ProtonSession session = remoteSessions.get(channel);
+        if (session == null) {
+            engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on Flow from remote: " + channel));
+        } else {
+            session.remoteFlow(flow, channel);
+        }
+    }
+
+    @Override
+    public void handleTransfer(Transfer transfer, ProtonBuffer payload, int channel, ProtonEngine context) {
+        final ProtonSession session = remoteSessions.get(channel);
+        if (session == null) {
+            engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on Transfer from remote: " + channel));
+        } else {
+            session.remoteTransfer(transfer, payload, channel);
+        }
+    }
+
+    @Override
+    public void handleDisposition(Disposition disposition, ProtonBuffer payload, int channel, ProtonEngine context) {
+        final ProtonSession session = remoteSessions.get(channel);
+        if (session == null) {
+            engine.engineFailed(new ProtocolViolationException("Received uncorrelated channel on Disposition from remote: " + channel));
+        } else {
+            session.remoteDispsotion(disposition, channel);
+        }
+    }
+
+    //----- API for event handling of Connection related remote events
+
+    @Override
+    public ProtonConnection sessionOpenHandler(EventHandler<Session> remoteSessionOpenEventHandler) {
+        this.remoteSessionOpenEventHandler = remoteSessionOpenEventHandler;
+        return this;
+    }
+
+    @Override
+    public ProtonConnection senderOpenHandler(EventHandler<Sender> remoteSenderOpenEventHandler) {
+        this.remoteSenderOpenEventHandler = remoteSenderOpenEventHandler;
+        return this;
+    }
+
+    EventHandler<Sender> senderOpenEventHandler() {
+        return remoteSenderOpenEventHandler;
+    }
+
+    @Override
+    public ProtonConnection receiverOpenHandler(EventHandler<Receiver> remoteReceiverOpenEventHandler) {
+        this.remoteReceiverOpenEventHandler = remoteReceiverOpenEventHandler;
+        return this;
+    }
+
+    EventHandler<Receiver> receiverOpenEventHandler() {
+        return remoteReceiverOpenEventHandler;
+    }
+
+    @Override
+    public ProtonConnection transactionManagerOpenHandler(EventHandler<TransactionManager> remoteTxnManagerOpenEventHandler) {
+        this.remoteTxnManagerOpenEventHandler = remoteTxnManagerOpenEventHandler;
+        return this;
+    }
+
+    EventHandler<TransactionManager> transactionManagerOpenHandler() {
+        return remoteTxnManagerOpenEventHandler;
+    }
+
+    //----- Internal implementation
+
+    private void checkNotOpened(String errorMessage) {
+        if (localState.ordinal() > ConnectionState.IDLE.ordinal()) {
+            throw new IllegalStateException(errorMessage);
+        }
+    }
+
+    private void checkConnectionClosed(String errorMessage) {
+        if (isLocallyClosed() || isRemotelyClosed()) {
+             throw new IllegalStateException(errorMessage);
+        }
+    }
+
+    private void syncLocalStateWithRemote() {
+        if (engine.isWritable()) {
+            // When the engine state changes or we have read an incoming AMQP header etc we need to check
+            // if we have pending work to send and do so
+            if (headerSent) {
+                final ConnectionState state = getState();
+
+                // Once an incoming header arrives we can emit our open if locally opened and also send close if
+                // that is what our state is already.
+                if (state != ConnectionState.IDLE && remoteHeader != null) {
+                    boolean resourceSyncNeeded = false;
+
+                    if (!localOpenSent && !engine.isShutdown()) {
+                        engine.fireWrite(localOpen, 0);
+                        engine.configuration().recomputeEffectiveFrameSizeLimits();
+                        localOpenSent = true;
+                        resourceSyncNeeded = true;
+                    }
+
+                    if (isLocallyClosed() && !localCloseSent && !engine.isShutdown()) {
+                        Close localClose = new Close().setError(getCondition());
+                        engine.fireWrite(localClose, 0);
+                        localCloseSent = true;
+                        resourceSyncNeeded = false;  // Session resources can't write anything now
+                    }
+
+                    if (resourceSyncNeeded) {
+                        allSessions().forEach(session -> session.trySyncLocalStateWithRemote());
+                    }
+                }
+            } else if (remoteHeader != null || getState() == ConnectionState.ACTIVE || remoteHeaderHandler != null) {
+                headerSent = true;
+                engine.fireWrite(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+            }
+        }
+    }
+
+    void handleEngineStarted(ProtonEngine protonEngine) {
+        syncLocalStateWithRemote();
+    }
+
+    void handleEngineShutdown(ProtonEngine protonEngine) {
+        try {
+            fireEngineShutdown();
+        } catch (Exception ignore) {}
+
+        allSessions().forEach(session -> session.handleEngineShutdown(protonEngine));
+    }
+
+    void handleEngineFailed(ProtonEngine protonEngine, Throwable cause) {
+        if (localOpenSent && !localCloseSent) {
+            localCloseSent = true;
+
+            try {
+                if (getCondition() == null) {
+                    setCondition(errorConditionFromFailureCause(cause));
+                }
+
+                engine.fireWrite(new Close().setError(getCondition()), 0);
+            } catch (Exception ignore) {}
+        }
+    }
+
+    private ErrorCondition errorConditionFromFailureCause(Throwable cause) {
+        final Symbol condition;
+        final String description = cause.getMessage();
+
+        if (cause instanceof ProtocolViolationException) {
+            ProtocolViolationException error = (ProtocolViolationException) cause;
+            condition = error.getErrorCondition();
+        } else {
+            condition = AmqpError.INTERNAL_ERROR;
+        }
+
+        return new ErrorCondition(condition, description);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Set<ProtonSession> allSessions() {
+        final Set<ProtonSession> result;
+
+        if (localSessions.isEmpty() && remoteSessions.isEmpty()) {
+            result = Collections.EMPTY_SET;
+        } else {
+            result = new HashSet<>(localSessions.size());
+            result.addAll(localSessions.values());
+            result.addAll(remoteSessions.values());
+        }
+
+        return result;
+    }
+
+    private int findFreeLocalChannel() {
+        for (int i = 0; i <= localOpen.getChannelMax(); ++i) {
+            if (!localSessions.containsKey(i) && !zombieSessions.containsKey(i)) {
+                return i;
+            }
+        }
+
+        // We didn't find one that isn't free and also not awaiting remote being / end
+        // so just use an overlap as it should complete in order unles the remote has
+        // completely ignored the specification and or gone of the rails.
+        for (int i = 0; i <= localOpen.getChannelMax(); ++i) {
+            if (!localSessions.containsKey(i)) {
+                return i;
+            }
+        }
+
+        throw new IllegalStateException("no local channel available for allocation");
+    }
+
+    void freeLocalChannel(int localChannel) {
+        if (localChannel > ProtonConstants.CHANNEL_MAX) {
+            throw new IllegalArgumentException("Specified local channel is out of range: " + localChannel);
+        }
+
+        ProtonSession session = localSessions.remove(localChannel);
+        if (session.getRemoteState() == SessionState.IDLE) {
+            // The remote hasn't answered our begin yet so we need to hold onto this information
+            // and process the eventual begin that must be provided per specification.
+            zombieSessions.put(localChannel, new SoftReference<>(session));
+        }
+    }
+
+    boolean wasHeaderSent() {
+        return this.headerSent;
+    }
+
+    boolean wasLocalOpenSent() {
+        return this.localOpenSent;
+    }
+
+    boolean wasLocalCloseSent() {
+        return this.localCloseSent;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonConstants.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonConstants.java
new file mode 100644
index 0000000..1934acf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonConstants.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+/**
+ * Constants referenced throughout the proton engine code.
+ */
+public final class ProtonConstants {
+
+    /**
+     * The minimum allowed AMQP maximum frame size defined by the specification.
+     */
+    public static final int MIN_MAX_AMQP_FRAME_SIZE = 512;
+
+    /**
+     * The default AMQP max frame size used by the engine and connection if none is set
+     * by the client or remote peer.
+     */
+    public static final int DEFAULT_MAX_AMQP_FRAME_SIZE = 65535;
+
+    /**
+     * The maximum value for AMQP channels as defined by the specification.
+     */
+    public static final int CHANNEL_MAX = 65535;
+
+    /**
+     * The maximum value for AMQP handles as defined by the specification.
+     */
+    public static final long HANDLE_MAX = 0xFFFFFFFFL;
+
+    //----- Proton engine handler names
+
+    /**
+     * Engine handler that acts on AMQP performatives
+     */
+    public static final String AMQP_PERFORMATIVE_HANDLER = "amqp";
+
+    /**
+     * Engine handler that acts on SASL performatives
+     */
+    public static final String SASL_PERFORMATIVE_HANDLER = "sasl";
+
+    /**
+     * Engine handler that encodes performatives and writes the resulting buffer
+     */
+    public static final String FRAME_ENCODING_HANDLER = "frame-encoder";
+
+    /**
+     * Engine handler that decodes performatives and forwards the frames
+     */
+    public static final String FRAME_DECODING_HANDLER = "frame-decoder";
+
+    /**
+     * Engine handler that logs incoming and outgoing performatives and frames
+     */
+    public static final String FRAME_LOGGING_HANDLER = "frame-logger";
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonDeliveryTagGenerator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonDeliveryTagGenerator.java
new file mode 100644
index 0000000..1558bae
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonDeliveryTagGenerator.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+
+/**
+ * Proton provided {@link DeliveryTagGenerator} utility.
+ */
+public abstract class ProtonDeliveryTagGenerator implements DeliveryTagGenerator {
+
+    private static final ProtonEmptyTagGenerator EMPTY_TAG_GENERATOR = new ProtonEmptyTagGenerator();
+
+    public enum BUILTIN {
+        /**
+         * Provides a {@link DeliveryTagGenerator} that creates tags based on an incrementing
+         * numeric value starting from zero and moving upwards until the value wraps and continue
+         * back towards zero.
+         */
+        SEQUENTIAL {
+
+            @Override
+            public DeliveryTagGenerator createGenerator() {
+                return new ProtonSequentialTagGenerator();
+            }
+        },
+        /**
+         * Provides a {@link DeliveryTagGenerator} that creates tags based on a UUID value that
+         * will be written as two long value encoded into the delivery tag bytes.
+         */
+        UUID {
+
+            @Override
+            public DeliveryTagGenerator createGenerator() {
+                return new ProtonUuidTagGenerator();
+            }
+        },
+        /**
+         * Provides a {@link DeliveryTagGenerator} that uses a pool of {@link DeliveryTag} instances
+         * in an attempt to reduce GC overhead on Delivery sends.  The tags are created using in numeric
+         * base value that is incremented as new tag values are requested and none can be prodices from
+         * the tag pool.
+         */
+        POOLED {
+
+            @Override
+            public DeliveryTagGenerator createGenerator() {
+                return new ProtonPooledTagGenerator();
+            }
+        },
+        /**
+         * Provides a {@link DeliveryTagGenerator} that returns a singleton empty tag value that can be
+         * used by senders that are sending settled deliveries and simply need to provide a non-null tag
+         * value to the outgoing delivery instance.
+         */
+        EMPTY {
+
+            @Override
+            public DeliveryTagGenerator createGenerator() {
+                return EMPTY_TAG_GENERATOR;
+            }
+        };
+
+        public abstract DeliveryTagGenerator createGenerator();
+
+    }
+
+    private static final class ProtonEmptyTagGenerator implements DeliveryTagGenerator {
+
+        private static final byte[] EMPTY_BYTE_ARRAY = new byte[] {};
+        private static final DeliveryTag EMPTY_DELIVERY_TAG = new DeliveryTag.ProtonDeliveryTag(EMPTY_BYTE_ARRAY);
+
+        @Override
+        public DeliveryTag nextTag() {
+            return EMPTY_DELIVERY_TAG;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEndpoint.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEndpoint.java
new file mode 100644
index 0000000..3bc98c8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEndpoint.java
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.engine.Endpoint;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Proton abstract {@link Endpoint} implementation that provides some common facilities.
+ *
+ * @param <E> The specific {@link Endpoint} type this abstract type implements.
+ */
+public abstract class ProtonEndpoint<E extends Endpoint<E>> implements Endpoint<E> {
+
+    protected final ProtonEngine engine;
+
+    private ProtonAttachments attachments;
+    private Object linkedResource;
+
+    private ErrorCondition localError;
+    private ErrorCondition remoteError;
+
+    private EventHandler<E> remoteOpenHandler;
+    private EventHandler<E> remoteCloseHandler;
+    private EventHandler<E> localOpenHandler;
+    private EventHandler<E> localCloseHandler;
+    private EventHandler<Engine> engineShutdownHandler;
+
+    /**
+     * Create a new {@link ProtonEndpoint} instance with the given Engine as the owner.
+     *
+     * @param engine
+     *      The {@link Engine} that this {@link Endpoint} belongs to.
+     */
+    public ProtonEndpoint(ProtonEngine engine) {
+        this.engine = engine;
+    }
+
+    @Override
+    public E setLinkedResource(Object resource) {
+        this.linkedResource = resource;
+        return self();
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T getLinkedResource() {
+        return (T) linkedResource;
+    }
+
+    @Override
+    public <T> T getLinkedResource(Class<T> typeClass) {
+        return typeClass.cast(linkedResource);
+    }
+
+    @Override
+    public ProtonEngine getEngine() {
+        return engine;
+    }
+
+    @Override
+    public ProtonAttachments getAttachments() {
+        return attachments == null ? attachments = new ProtonAttachments() : attachments;
+    }
+
+    @Override
+    public ErrorCondition getCondition() {
+        return localError;
+    }
+
+    @Override
+    public E setCondition(ErrorCondition condition) {
+        localError = condition == null ? null : condition.copy();
+        return self();
+    }
+
+    @Override
+    public ErrorCondition getRemoteCondition() {
+        return remoteError;
+    }
+
+    E setRemoteCondition(ErrorCondition condition) {
+        remoteError = condition == null ? null : condition.copy();
+        return self();
+    }
+
+    //----- Abstract methods
+
+    abstract E self();
+
+    //----- Event handler registration and access
+
+    @Override
+    public E openHandler(EventHandler<E> remoteOpenEventHandler) {
+        this.remoteOpenHandler = remoteOpenEventHandler;
+        return self();
+    }
+
+    EventHandler<E> openHandler() {
+        return remoteOpenHandler;
+    }
+
+    E fireRemoteOpen() {
+        if (remoteOpenHandler != null) {
+            remoteOpenHandler.handle(self());
+        }
+
+        return self();
+    }
+
+    @Override
+    public E closeHandler(EventHandler<E> remoteCloseEventHandler) {
+        this.remoteCloseHandler = remoteCloseEventHandler;
+        return self();
+    }
+
+    EventHandler<E> closeHandler() {
+        return remoteCloseHandler;
+    }
+
+    E fireRemoteClose() {
+        if (remoteCloseHandler != null) {
+            remoteCloseHandler.handle(self());
+        }
+
+        return self();
+    }
+
+    @Override
+    public E localOpenHandler(EventHandler<E> localOpenEventHandler) {
+        this.localOpenHandler = localOpenEventHandler;
+        return self();
+    }
+
+    EventHandler<E> localOpenHandler() {
+        return localOpenHandler;
+    }
+
+    E fireLocalOpen() {
+        if (localOpenHandler != null) {
+            localOpenHandler.handle(self());
+        }
+
+        return self();
+    }
+
+    @Override
+    public E localCloseHandler(EventHandler<E> localCloseEventHandler) {
+        this.localCloseHandler = localCloseEventHandler;
+        return self();
+    }
+
+    EventHandler<E> localCloseHandler() {
+        return localCloseHandler;
+    }
+
+    E fireLocalClose() {
+        if (localCloseHandler != null) {
+            localCloseHandler.handle(self());
+        }
+
+        return self();
+    }
+
+    @Override
+    public E engineShutdownHandler(EventHandler<Engine> engineShutdownEventHandler) {
+        this.engineShutdownHandler = engineShutdownEventHandler;
+        return self();
+    }
+
+    EventHandler<Engine> engineShutdownHandler() {
+        return engineShutdownHandler;
+    }
+
+    Engine fireEngineShutdown() {
+        if (engineShutdownHandler != null) {
+            engineShutdownHandler.handle(engine);
+        }
+
+        return engine;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngine.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngine.java
new file mode 100644
index 0000000..c6ba04c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngine.java
@@ -0,0 +1,521 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.AMQPPerformativeEnvelopePool;
+import org.apache.qpid.protonj2.engine.ConnectionState;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EnginePipeline;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineNotStartedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineNotWritableException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineShutdownException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStartedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.IdleTimeoutException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtonExceptionSupport;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Performative;
+
+/**
+ * The default proton Engine implementation.
+ */
+public class ProtonEngine implements Engine {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonEngine.class);
+
+    private static final ProtonBuffer EMPTY_FRAME_BUFFER =
+        ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00});
+
+    private final ProtonEnginePipeline pipeline =  new ProtonEnginePipeline(this);
+    private final ProtonEnginePipelineProxy pipelineProxy = new ProtonEnginePipelineProxy(pipeline);
+    private final ProtonEngineConfiguration configuration = new ProtonEngineConfiguration(this);
+    private final ProtonConnection connection = new ProtonConnection(this);
+    private final AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> framePool = AMQPPerformativeEnvelopePool.outgoingEnvelopePool();
+
+    private EngineSaslDriver saslDriver = new ProtonEngineNoOpSaslDriver();
+
+    private boolean writable;
+    private EngineState state = EngineState.IDLE;
+    private Throwable failureCause;
+    private int inputSequence;
+    private int outputSequence;
+
+    // Idle Timeout Check data
+    private ScheduledFuture<?> nextIdleTimeoutCheck;
+    private ScheduledExecutorService idleTimeoutExecutor;
+    private int lastInputSequence;
+    private int lastOutputSequence;
+    private long localIdleDeadline = 0;
+    private long remoteIdleDeadline = 0;
+
+    // Engine event points
+    private BiConsumer<ProtonBuffer, Runnable> outputHandler;
+    private EventHandler<Engine> engineShutdownHandler;
+    private EventHandler<Engine> engineFailureHandler = (engine) -> {
+        LOG.warn("Engine encounted error and will become inoperable: ", engine.failureCause());
+    };
+
+    @Override
+    public ProtonConnection connection() {
+        return connection;
+    }
+
+    @Override
+    public boolean isWritable() {
+        return writable;
+    }
+
+    @Override
+    public boolean isRunning() {
+        return state == EngineState.STARTED;
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return state.ordinal() >= EngineState.SHUTDOWN.ordinal();
+    }
+
+    @Override
+    public boolean isFailed() {
+        return failureCause != null;
+    }
+
+    @Override
+    public Throwable failureCause() {
+        return failureCause;
+    }
+
+    @Override
+    public EngineState state() {
+        return state;
+    }
+
+    @Override
+    public ProtonConnection start() throws EngineStateException {
+        checkShutdownOrFailed("Cannot start an Engine that has already been shutdown or has failed.");
+
+        if (state == EngineState.IDLE) {
+            state = EngineState.STARTING;
+            try {
+                pipeline.fireEngineStarting();
+                state = EngineState.STARTED;
+                writable = true;
+                connection.handleEngineStarted(this);
+            } catch (Throwable error) {
+                throw engineFailed(error);
+            }
+        }
+
+        return connection;
+    }
+
+    @Override
+    public ProtonEngine shutdown() {
+        if (state.ordinal() < EngineState.SHUTTING_DOWN.ordinal()) {
+            state = EngineState.SHUTDOWN;
+            writable = false;
+
+            if (nextIdleTimeoutCheck != null) {
+                LOG.trace("Cancelling scheduled Idle Timeout Check");
+                nextIdleTimeoutCheck.cancel(false);
+                nextIdleTimeoutCheck = null;
+            }
+
+            try {
+                pipeline.fireEngineStateChanged();
+            } catch (Exception ignored) {}
+
+            try {
+                connection.handleEngineShutdown(this);
+            } catch (Exception ignored) {
+            } finally {
+                if (engineShutdownHandler != null) {
+                    engineShutdownHandler.handle(this);
+                }
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public long tick(long currentTime) throws IllegalStateException, EngineStateException {
+        checkShutdownOrFailed("Cannot tick an Egnine that has been shutdown or failed.");
+
+        if (connection.getState() != ConnectionState.ACTIVE) {
+            throw new IllegalStateException("Cannot tick on a Connection that is not opened or an engine that has been shut down.");
+        }
+
+        if (idleTimeoutExecutor != null) {
+            throw new IllegalStateException("Automatic ticking previously initiated.");
+        }
+
+        performReadCheck(currentTime);
+        performWriteCheck(currentTime);
+
+        return nextTickDeadline(localIdleDeadline, remoteIdleDeadline);
+    }
+
+    @Override
+    public ProtonEngine tickAuto(ScheduledExecutorService executor) throws IllegalStateException, EngineStateException {
+        checkShutdownOrFailed("Cannot start auto tick on an Engine that has been shutdown or failed");
+
+        Objects.requireNonNull(executor);
+
+        if (connection.getState() != ConnectionState.ACTIVE) {
+            throw new IllegalStateException("Cannot tick on a Connection that is not opened.");
+        }
+
+        if (idleTimeoutExecutor != null) {
+            throw new IllegalStateException("Automatic ticking previously initiated.");
+        }
+
+        // TODO - As an additional feature of this method we could allow for calling before connection is
+        //        opened such that it starts ticking either on open local and also checks as a response to
+        //        remote open which seems might be needed anyway, see notes in IdleTimeoutCheck class.
+
+        // Immediate run of the idle timeout check logic will decide afterwards when / if we should
+        // reschedule the idle timeout processing.
+        LOG.trace("Auto Idle Timeout Check being initiated");
+        idleTimeoutExecutor = executor;
+        idleTimeoutExecutor.execute(new IdleTimeoutCheck());
+
+        return this;
+    }
+
+    @Override
+    public ProtonEngine ingest(ProtonBuffer input) throws EngineStateException {
+        checkShutdownOrFailed("Cannot ingest data into an Engine that has been shutdown or failed");
+
+        if (!isWritable()) {
+            throw new EngineNotWritableException("Engine is currently not accepting new input");
+        }
+
+        try {
+            int startIndex = input.getReadIndex();
+            pipeline.fireRead(input);
+            if (input.getReadIndex() != startIndex) {
+                inputSequence++;
+            }
+        } catch (Exception error) {
+            throw engineFailed(error);
+        }
+
+        return this;
+    }
+
+    @Override
+    public EngineStateException engineFailed(Throwable cause) {
+        final EngineStateException failure;
+
+        if (state.ordinal() < EngineState.SHUTTING_DOWN.ordinal() && state != EngineState.FAILED) {
+            state = EngineState.FAILED;
+            failureCause = cause;
+            writable = false;
+
+            if (nextIdleTimeoutCheck != null) {
+                LOG.trace("Cancelling scheduled Idle Timeout Check");
+                nextIdleTimeoutCheck.cancel(false);
+                nextIdleTimeoutCheck = null;
+            }
+
+            failure = ProtonExceptionSupport.createFailedException(cause);
+
+            try {
+                pipeline.fireFailed((EngineFailedException) failure);
+            } catch (Exception ignored) {}
+
+            try {
+                connection.handleEngineFailed(this, cause);
+            } catch (Exception ignored) {
+            }
+
+            engineFailureHandler.handle(this);
+        } else {
+            if (isFailed()) {
+                failure = ProtonExceptionSupport.createFailedException(cause);
+            } else {
+                failure = new EngineShutdownException("Engine has transitioned to shutdown state");
+            }
+        }
+
+        return failure;
+    }
+
+    //----- Engine configuration
+
+    @Override
+    public ProtonEngine outputHandler(BiConsumer<ProtonBuffer, Runnable> handler) {
+        this.outputHandler = handler;
+        return this;
+    }
+
+    BiConsumer<ProtonBuffer, Runnable> outputHandler() {
+        return outputHandler;
+    }
+
+    @Override
+    public ProtonEngine errorHandler(EventHandler<Engine> handler) {
+        this.engineFailureHandler = handler;
+        return this;
+    }
+
+    EventHandler<Engine> errorHandler() {
+        return engineFailureHandler;
+    }
+
+    @Override
+    public ProtonEngine shutdownHandler(EventHandler<Engine> handler) {
+        this.engineShutdownHandler = handler;
+        return this;
+    }
+
+    EventHandler<Engine> engineShutdownHandler() {
+        return engineShutdownHandler;
+    }
+
+    @Override
+    public EnginePipeline pipeline() {
+        return pipelineProxy;
+    }
+
+    @Override
+    public ProtonEngineConfiguration configuration() {
+        return configuration;
+    }
+
+    @Override
+    public EngineSaslDriver saslDriver() {
+        return saslDriver;
+    }
+
+    /**
+     * Allows for registration of a custom {@link EngineSaslDriver} that will convey
+     * SASL state and configuration for this engine.
+     *
+     * @param saslDriver
+     *      The {@link EngineSaslDriver} that this engine will use.
+     *
+     * @throws EngineStateException if the engine state doesn't allow for changes
+     */
+    public void registerSaslDriver(EngineSaslDriver saslDriver) throws EngineStateException {
+        checkShutdownOrFailed("Cannot register a SASL driver on an Engine that is shutdown or failed.");
+
+        if (state.ordinal() > EngineState.STARTING.ordinal()) {
+            throw new EngineStartedException("Cannot alter SASL driver after Engine has been started.");
+        }
+
+        this.saslDriver = saslDriver;
+    }
+
+    //----- Internal proton engine implementation
+
+    ProtonEngine fireWrite(HeaderEnvelope frame) {
+        pipeline.fireWrite(frame);
+        return this;
+    }
+
+    ProtonEngine fireWrite(OutgoingAMQPEnvelope frame) {
+        pipeline.fireWrite(frame);
+        return this;
+    }
+
+    ProtonEngine fireWrite(Performative performative, int channel) {
+        pipeline.fireWrite(framePool.take(performative, channel, null));
+        return this;
+    }
+
+    ProtonEngine fireWrite(Performative performative, int channel, ProtonBuffer payload) {
+        pipeline.fireWrite(framePool.take(performative, channel, payload));
+        return this;
+    }
+
+    OutgoingAMQPEnvelope wrap(Performative performative, int channel, ProtonBuffer payload) {
+        return framePool.take(performative, channel, payload);
+    }
+
+    void checkEngineNotStarted(String message) {
+        if (state == EngineState.IDLE) {
+            throw new EngineNotStartedException(message);
+        }
+    }
+
+    void checkFailed(String message) {
+        if (state == EngineState.FAILED) {
+            throw ProtonExceptionSupport.createFailedException(message, failureCause);
+        }
+    }
+
+    void checkShutdownOrFailed(String message) {
+        if (state.ordinal() > EngineState.STARTED.ordinal()) {
+            if (isFailed()) {
+                throw ProtonExceptionSupport.createFailedException(message, failureCause);
+            } else {
+                throw new EngineShutdownException(message);
+            }
+        }
+    }
+
+    void dispatchWriteToEventHandler(ProtonBuffer buffer, Runnable ioComplete) {
+        if (outputHandler != null) {
+            outputSequence++;
+            try {
+                outputHandler.accept(buffer, ioComplete);
+            } catch (Throwable error) {
+                throw engineFailed(error);
+            }
+        } else {
+            throw engineFailed(new IllegalStateException("No output handler configured"));
+        }
+    }
+
+    //----- Idle Timeout processing methods and inner classes
+
+    private void performReadCheck(long currentTime) {
+        long localIdleTimeout = connection.getIdleTimeout();
+
+        if (localIdleTimeout > 0) {
+            if (localIdleDeadline == 0 || lastInputSequence != inputSequence) {
+                localIdleDeadline = computeDeadline(currentTime, localIdleTimeout);
+                lastInputSequence = inputSequence;
+            } else if (localIdleDeadline - currentTime <= 0) {
+                if (connection.getState() != ConnectionState.CLOSED) {
+                    ErrorCondition condition = new ErrorCondition(
+                        Symbol.getSymbol("amqp:resource-limit-exceeded"), "local-idle-timeout expired");
+                    connection.setCondition(condition);
+                    connection.close();
+                    engineFailed(new IdleTimeoutException("Remote idle timeout detected"));
+                } else {
+                    localIdleDeadline = computeDeadline(currentTime, localIdleTimeout);
+                }
+            }
+        }
+    }
+
+    private void performWriteCheck(long currentTime) {
+        long remoteIdleTimeout = connection.getRemoteIdleTimeout();
+
+        if (remoteIdleTimeout > 0 && !connection.isLocallyClosed()) {
+            if (remoteIdleDeadline == 0 || lastOutputSequence != outputSequence) {
+                remoteIdleDeadline = computeDeadline(currentTime, remoteIdleTimeout / 2);
+                lastOutputSequence = outputSequence;
+            } else if (remoteIdleDeadline - currentTime <= 0) {
+                remoteIdleDeadline = computeDeadline(currentTime, remoteIdleTimeout / 2);
+                pipeline.fireWrite(EMPTY_FRAME_BUFFER.duplicate(), null);
+                lastOutputSequence++;
+            }
+        }
+    }
+
+    private long computeDeadline(long now, long timeout) {
+        long deadline = now + timeout;
+        // We use 0 to signal not-initialised and/or no-timeout, so in the
+        // unlikely event thats to be the actual deadline, return 1 instead
+        return deadline != 0 ? deadline : 1;
+    }
+
+    private static long nextTickDeadline(long localIdleDeadline, long remoteIdleDeadline) {
+        final long deadline;
+
+        // If there is no locally set idle timeout then we just honor the remote idle timeout
+        // value otherwise we need to use the lesser of the next local or remote idle timeout
+        // deadline values to compute the next time a check is needed.
+        if (localIdleDeadline == 0) {
+             deadline = remoteIdleDeadline;
+        } else if (remoteIdleDeadline == 0) {
+            deadline = localIdleDeadline;
+        } else {
+            if (remoteIdleDeadline - localIdleDeadline <= 0) {
+                deadline = remoteIdleDeadline;
+            } else {
+                deadline = localIdleDeadline;
+            }
+        }
+
+        return deadline;
+    }
+
+    private final class IdleTimeoutCheck implements Runnable {
+
+        // TODO - Pick reasonable values
+        private final long MIN_IDLE_CHECK_INTERVAL = 1000;
+        private final long MAX_IDLE_CHECK_INTERVAL = 10000;
+
+        @Override
+        public void run() {
+            boolean checkScheduled = false;
+
+            if (connection.getState() == ConnectionState.ACTIVE && !isShutdown()) {
+                // Using nano time since it is not related to the wall clock, which may change
+                long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+                try {
+                    performReadCheck(now);
+                    performWriteCheck(now);
+
+                    final long deadline = nextTickDeadline(localIdleDeadline, remoteIdleDeadline);
+
+                    // Check methods will close down the engine and fire error so we need to check that engine
+                    // state is active and engine is not shutdown before scheduling again.
+                    if (deadline != 0 && connection.getState() == ConnectionState.ACTIVE && state() == EngineState.STARTED) {
+                        // Run the next idle check at half the deadline to try and ensure we meet our
+                        // obligation of sending our heart beat on time.
+                        long delay = (deadline - now) / 2;
+
+                        // TODO - Some computation to work out a reasonable delay that still compensates for
+                        //        errors in scheduling while preventing over eagerness.
+                        delay = Math.max(MIN_IDLE_CHECK_INTERVAL, delay);
+                        delay = Math.min(MAX_IDLE_CHECK_INTERVAL, delay);
+
+                        checkScheduled = true;
+                        LOG.trace("IdleTimeoutCheck rescheduling with delay: {}", delay);
+                        nextIdleTimeoutCheck = idleTimeoutExecutor.schedule(this, delay, TimeUnit.MILLISECONDS);
+                    }
+
+                    // TODO - If no local timeout but remote hasn't opened we might return zero and not
+                    //        schedule any ticking ?  Possible solution is to schedule after remote open
+                    //        arrives if nothing set to run and remote indicates it has an idle timeout.
+
+                } catch (Throwable t) {
+                    LOG.trace("Auto Idle Timeout Check encountered error during check: ", t);
+                }
+            }
+
+            if (!checkScheduled) {
+                nextIdleTimeoutCheck = null;
+                LOG.trace("Auto Idle Timeout Check task exiting and will not be rescheduled");
+            }
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineConfiguration.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineConfiguration.java
new file mode 100644
index 0000000..fb050f5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineConfiguration.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.EngineConfiguration;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Proton engine configuration API
+ */
+public class ProtonEngineConfiguration implements EngineConfiguration {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonEngineConfiguration.class);
+
+    private final ProtonEngine engine;
+
+    private ProtonBufferAllocator allocator = ProtonByteBufferAllocator.DEFAULT;
+
+    private long effectiveMaxInboundFrameSize = ProtonConstants.MIN_MAX_AMQP_FRAME_SIZE;
+    private long effectiveMaxOutboundFrameSize = ProtonConstants.MIN_MAX_AMQP_FRAME_SIZE;
+
+    ProtonEngineConfiguration(ProtonEngine engine) {
+        this.engine = engine;
+    }
+
+    @Override
+    public ProtonBufferAllocator getBufferAllocator() {
+        return allocator;
+    }
+
+    @Override
+    public ProtonEngineConfiguration setBufferAllocator(ProtonBufferAllocator allocator) {
+        this.allocator = allocator;
+        return this;
+    }
+
+
+    @Override
+    public EngineConfiguration setTraceFrames(boolean traceFrames) {
+        // If the frame logging handler wasn't added or was removed for less overhead then
+        // the setting will have no effect and isTraceFrames will always return false
+        EngineHandler handler = engine.pipeline().find(ProtonConstants.FRAME_LOGGING_HANDLER);
+        if (handler != null && handler instanceof ProtonFrameLoggingHandler) {
+            ((ProtonFrameLoggingHandler) handler).setTraceFrames(traceFrames);
+        } else {
+            LOG.debug("Engine not configured with a frame logging handler: cannot apply traceFrames={}", traceFrames);
+        }
+
+        return this;
+    }
+
+    @Override
+    public boolean isTraceFrames() {
+        EngineHandler handler = engine.pipeline().find(ProtonConstants.FRAME_LOGGING_HANDLER);
+        if (handler != null && handler instanceof ProtonFrameLoggingHandler) {
+            return ((ProtonFrameLoggingHandler) handler).isTraceFrames();
+        } else {
+            return false;
+        }
+    }
+
+    //---- proton specific APIs
+
+    void recomputeEffectiveFrameSizeLimits() {
+        // Based on engine state compute what the max in and out frame size should
+        // be at this time.  Considerations to take into account are SASL state and
+        // remote values once set.
+
+        if (engine.saslDriver().getSaslState().ordinal() < SaslState.AUTHENTICATED.ordinal()) {
+            effectiveMaxInboundFrameSize = engine.saslDriver().getMaxFrameSize();
+            effectiveMaxOutboundFrameSize = engine.saslDriver().getMaxFrameSize();
+        } else {
+            final long localMaxFrameSize = engine.connection().getMaxFrameSize();
+            final long remoteMaxFrameSize = engine.connection().getRemoteMaxFrameSize();
+
+            // We limit outbound max frame size to our own set max frame size unless the remote has actually
+            // requested something smaller as opposed to just using a default like 2GB or something similarly
+            // large which we could never support in practice.
+            final long intermediateMaxOutboundFrameSize = Math.min(localMaxFrameSize, remoteMaxFrameSize);
+
+            effectiveMaxInboundFrameSize = Math.min(UnsignedInteger.MAX_VALUE.longValue(), engine.connection().getMaxFrameSize());
+
+            effectiveMaxOutboundFrameSize = Math.min(UnsignedInteger.MAX_VALUE.longValue(), intermediateMaxOutboundFrameSize);
+        }
+    }
+
+    long getOutboundMaxFrameSize() {
+        return effectiveMaxOutboundFrameSize;
+    }
+
+    long getInboundMaxFrameSize() {
+        return effectiveMaxInboundFrameSize;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineFactory.java
new file mode 100644
index 0000000..0f8600d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.EnginePipeline;
+import org.apache.qpid.protonj2.engine.impl.sasl.ProtonSaslHandler;
+
+/**
+ * Factory class for proton Engine creation
+ */
+public final class ProtonEngineFactory implements EngineFactory {
+
+    @Override
+    public Engine createEngine() {
+        ProtonEngine engine = new ProtonEngine();
+        EnginePipeline pipeline = engine.pipeline();
+
+        pipeline.addLast(ProtonConstants.AMQP_PERFORMATIVE_HANDLER, new ProtonPerformativeHandler());
+        pipeline.addLast(ProtonConstants.SASL_PERFORMATIVE_HANDLER, new ProtonSaslHandler());
+        pipeline.addLast(ProtonConstants.FRAME_LOGGING_HANDLER, new ProtonFrameLoggingHandler());
+        pipeline.addLast(ProtonConstants.FRAME_DECODING_HANDLER, new ProtonFrameDecodingHandler());
+        pipeline.addLast(ProtonConstants.FRAME_ENCODING_HANDLER, new ProtonFrameEncodingHandler());
+
+        return engine;
+   }
+
+    @Override
+    public Engine createNonSaslEngine() {
+        ProtonEngine engine = new ProtonEngine();
+        EnginePipeline pipeline = engine.pipeline();
+
+        pipeline.addLast(ProtonConstants.AMQP_PERFORMATIVE_HANDLER, new ProtonPerformativeHandler());
+        pipeline.addLast(ProtonConstants.FRAME_LOGGING_HANDLER, new ProtonFrameLoggingHandler());
+        pipeline.addLast(ProtonConstants.FRAME_DECODING_HANDLER, new ProtonFrameDecodingHandler());
+        pipeline.addLast(ProtonConstants.FRAME_ENCODING_HANDLER, new ProtonFrameEncodingHandler());
+
+        return engine;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineHandlerContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineHandlerContext.java
new file mode 100644
index 0000000..3a0245b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineHandlerContext.java
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+
+/**
+ * Context for a registered EngineHandler
+ */
+public class ProtonEngineHandlerContext implements EngineHandlerContext {
+
+    ProtonEngineHandlerContext previous;
+    ProtonEngineHandlerContext next;
+
+    public static final int HANDLER_READS = 1 << 1;
+    public static final int HANDLER_WRITES = 1 << 2;
+    public static final int HANDLER_ALL_EVENTS = HANDLER_READS | HANDLER_WRITES;
+
+    private final String name;
+    private final Engine engine;
+    private final EngineHandler handler;
+
+    private int intereskMask = HANDLER_ALL_EVENTS;
+
+    public ProtonEngineHandlerContext(String name, Engine engine, EngineHandler handler) {
+        this.name = name;
+        this.engine = engine;
+        this.handler = handler;
+    }
+
+    @Override
+    public EngineHandler handler() {
+        return handler;
+    }
+
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public Engine engine() {
+        return engine;
+    }
+
+    /**
+     * Allows a handler to indicate if it wants to be notified of a Engine Handler events for
+     * specific operations or opt into all engine handler events.  By opting out of the events
+     * that the handler does not process the call chain can be reduced when processing engine
+     * events.
+     *
+     * @return the interest mask that should be used to determine if a handler should be signaled.
+     */
+    public int interestMask() {
+        return intereskMask;
+    }
+
+    public ProtonEngineHandlerContext interestMask(int mask) {
+        this.intereskMask = mask;
+        return this;
+    }
+
+    @Override
+    public void fireEngineStarting() {
+        next.invokeEngineStarting();
+    }
+
+    @Override
+    public void fireEngineStateChanged() {
+        next.invokeEngineStateChanged();
+    }
+
+    @Override
+    public void fireFailed(EngineFailedException failure) {
+        next.invokeEngineFailed(failure);
+    }
+
+    @Override
+    public void fireRead(ProtonBuffer buffer) {
+        findNextReadHandler().invokeHandlerRead(buffer);
+    }
+
+    @Override
+    public void fireRead(HeaderEnvelope header) {
+        findNextReadHandler().invokeHandlerRead(header);
+    }
+
+    @Override
+    public void fireRead(SASLEnvelope envelope) {
+        findNextReadHandler().invokeHandlerRead(envelope);
+    }
+
+    @Override
+    public void fireRead(IncomingAMQPEnvelope envelope) {
+        findNextReadHandler().invokeHandlerRead(envelope);
+    }
+
+    @Override
+    public void fireWrite(OutgoingAMQPEnvelope envelope) {
+        findNextWriteHandler().invokeHandlerWrite(envelope);
+    }
+
+    @Override
+    public void fireWrite(SASLEnvelope envelope) {
+        findNextWriteHandler().invokeHandlerWrite(envelope);
+    }
+
+    @Override
+    public void fireWrite(HeaderEnvelope envelope) {
+        findNextWriteHandler().invokeHandlerWrite(envelope);
+    }
+
+    @Override
+    public void fireWrite(ProtonBuffer buffer, Runnable ioComplete) {
+        findNextWriteHandler().invokeHandlerWrite(buffer, ioComplete);
+    }
+
+    //----- Internal invoke of Engine and Handler state methods
+
+    void invokeEngineStarting() {
+        handler.engineStarting(this);
+    }
+
+    void invokeEngineStateChanged() {
+        handler.handleEngineStateChanged(this);
+    }
+
+    void invokeEngineFailed(EngineFailedException failure) {
+        handler.engineFailed(this, failure);
+    }
+
+    //----- Internal invoke of Read methods
+
+    void invokeHandlerRead(IncomingAMQPEnvelope envelope) {
+        handler.handleRead(this, envelope);
+    }
+
+    void invokeHandlerRead(SASLEnvelope envelope) {
+        handler.handleRead(this, envelope);
+    }
+
+    void invokeHandlerRead(HeaderEnvelope envelope) {
+        handler.handleRead(this, envelope);
+    }
+
+    void invokeHandlerRead(ProtonBuffer buffer) {
+        handler.handleRead(this, buffer);
+    }
+
+    //----- Internal invoke of Write methods
+
+    void invokeHandlerWrite(OutgoingAMQPEnvelope envelope) {
+        handler.handleWrite(this, envelope);
+    }
+
+    void invokeHandlerWrite(SASLEnvelope envelope) {
+        handler.handleWrite(this, envelope);
+    }
+
+    void invokeHandlerWrite(HeaderEnvelope envelope) {
+        handler.handleWrite(this, envelope);
+    }
+
+    void invokeHandlerWrite(ProtonBuffer buffer, Runnable ioComplete) {
+        next.handler().handleWrite(next, buffer, ioComplete);
+    }
+
+    private ProtonEngineHandlerContext findNextReadHandler() {
+        ProtonEngineHandlerContext ctx = this;
+        do {
+            ctx = ctx.previous;
+        } while (skipContext(ctx, HANDLER_READS));
+        return ctx;
+    }
+
+    private ProtonEngineHandlerContext findNextWriteHandler() {
+        ProtonEngineHandlerContext ctx = this;
+        do {
+            ctx = ctx.next;
+        } while (skipContext(ctx, HANDLER_WRITES));
+        return ctx;
+    }
+
+    private static boolean skipContext(ProtonEngineHandlerContext ctx, int interestMask) {
+        return (ctx.interestMask() & interestMask) == 0;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineNoOpSaslDriver.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineNoOpSaslDriver.java
new file mode 100644
index 0000000..374cd26
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineNoOpSaslDriver.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.engine.EngineSaslDriver;
+import org.apache.qpid.protonj2.engine.sasl.SaslClientContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslOutcome;
+import org.apache.qpid.protonj2.engine.sasl.SaslServerContext;
+
+/**
+ * A Default No-Op SASL context that is used to provide the engine with a stub
+ * when no SASL is configured for the operating engine.
+ */
+public final class ProtonEngineNoOpSaslDriver implements EngineSaslDriver {
+
+    public static final ProtonEngineNoOpSaslDriver INSTANCE = new ProtonEngineNoOpSaslDriver();
+
+    public static final int MIN_MAX_SASL_FRAME_SIZE = 512;
+
+    @Override
+    public SaslState getSaslState() {
+        return SaslState.NONE;
+    }
+
+    @Override
+    public SaslOutcome getSaslOutcome() {
+        return null;
+    }
+
+    @Override
+    public int getMaxFrameSize() {
+        return MIN_MAX_SASL_FRAME_SIZE;
+    }
+
+    @Override
+    public void setMaxFrameSize(int maxFrameSize) {
+    }
+
+    @Override
+    public SaslClientContext client() {
+        throw new IllegalStateException("Engine not configured with a SASL layer");
+    }
+
+    @Override
+    public SaslServerContext server() {
+        throw new IllegalStateException("Engine not configured with a SASL layer");
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipeline.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipeline.java
new file mode 100644
index 0000000..01df8ba
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipeline.java
@@ -0,0 +1,499 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.EnginePipeline;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtonException;
+
+/**
+ * Pipeline of {@link EngineHandler} instances used to process IO
+ */
+public class ProtonEnginePipeline implements EnginePipeline {
+
+    EngineHandlerContextReadBoundry head;
+    EngineHandlerContextWriteBoundry tail;
+
+    private final ProtonEngine engine;
+
+    ProtonEnginePipeline(ProtonEngine engine) {
+        if (engine == null) {
+            throw new IllegalArgumentException("Parent transport cannot be null");
+        }
+
+        this.engine = engine;
+
+        head = new EngineHandlerContextReadBoundry();
+        tail = new EngineHandlerContextWriteBoundry();
+
+        // Ensure Pipeline starts out empty but initialized.
+        head.next = tail;
+        head.previous = head;
+
+        tail.previous = head;
+        tail.next = tail;
+    }
+
+    @Override
+    public ProtonEngine engine() {
+        return engine;
+    }
+
+    @Override
+    public ProtonEnginePipeline addFirst(String name, EngineHandler handler) {
+        if (name == null || name.isEmpty()) {
+            throw new IllegalArgumentException("Handler name cannot be null or empty");
+        }
+
+        if (handler == null) {
+            throw new IllegalArgumentException("Handler provided cannot be null");
+        }
+
+        ProtonEngineHandlerContext oldFirst = head.next;
+        ProtonEngineHandlerContext newFirst = createContext(name, handler);
+
+        newFirst.next = oldFirst;
+        newFirst.previous = head;
+
+        oldFirst.previous = newFirst;
+        head.next = newFirst;
+
+        try {
+            newFirst.handler().handlerAdded(newFirst);
+        } catch (Throwable e) {
+            engine.engineFailed(e);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline addLast(String name, EngineHandler handler) {
+        if (name == null || name.isEmpty()) {
+            throw new IllegalArgumentException("Handler name cannot be null or empty");
+        }
+
+        if (handler == null) {
+            throw new IllegalArgumentException("Handler provided cannot be null");
+        }
+
+        ProtonEngineHandlerContext oldLast = tail.previous;
+        ProtonEngineHandlerContext newLast = createContext(name, handler);
+
+        newLast.next = tail;
+        newLast.previous = oldLast;
+
+        oldLast.next = newLast;
+        tail.previous = newLast;
+
+        try {
+            newLast.handler().handlerAdded(newLast);
+        } catch (Throwable e) {
+            engine.engineFailed(e);
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline removeFirst() {
+        if (head.next != tail) {
+            ProtonEngineHandlerContext oldFirst = head.next;
+
+            head.next = oldFirst.next;
+            head.next.previous = head;
+
+            try {
+                oldFirst.handler().handlerRemoved(oldFirst);
+            } catch (Throwable e) {
+                engine.engineFailed(e);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline removeLast() {
+        if (tail.previous != head) {
+            ProtonEngineHandlerContext oldLast = tail.previous;
+
+            tail.previous = oldLast.previous;
+            tail.previous.next = tail;
+
+            try {
+                oldLast.handler().handlerRemoved(oldLast);
+            } catch (Throwable e) {
+                engine.engineFailed(e);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline remove(String name) {
+        if (name != null && !name.isEmpty()) {
+            ProtonEngineHandlerContext current = head.next;
+            ProtonEngineHandlerContext removed = null;
+            while (current != tail) {
+                if (current.name().equals(name)) {
+                    removed = current;
+
+                    ProtonEngineHandlerContext newNext = current.next;
+
+                    current.previous.next = newNext;
+                    newNext.previous = current.previous;
+
+                    break;
+                }
+
+                current = current.next;
+            }
+
+            if (removed != null) {
+                try {
+                    removed.handler().handlerRemoved(removed);
+                } catch (Throwable e) {
+                    engine.engineFailed(e);
+                }
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public EnginePipeline remove(EngineHandler handler) {
+        if (handler != null) {
+            ProtonEngineHandlerContext current = head.next;
+            ProtonEngineHandlerContext removed = null;
+            while (current != tail) {
+                if (current.handler() == handler) {
+                    removed = current;
+
+                    ProtonEngineHandlerContext newNext = current.next;
+
+                    current.previous.next = newNext;
+                    newNext.previous = current.previous;
+
+                    break;
+                }
+
+                current = current.next;
+            }
+
+            if (removed != null) {
+                try {
+                    removed.handler().handlerRemoved(removed);
+                } catch (Throwable e) {
+                    engine.engineFailed(e);
+                }
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public EngineHandler find(String name) {
+        EngineHandler handler = null;
+
+        if (name != null && !name.isEmpty()) {
+            ProtonEngineHandlerContext current = head.next;
+            while (current != tail) {
+                if (current.name().equals(name)) {
+                    handler = current.handler();
+                    break;
+                }
+
+                current = current.next;
+            }
+        }
+
+        return handler;
+    }
+
+    @Override
+    public EngineHandler first() {
+        return head.next == tail ? null : head.next.handler();
+    }
+
+    @Override
+    public EngineHandler last() {
+        return tail.previous == head ? null : tail.previous.handler();
+    }
+
+    @Override
+    public EngineHandlerContext firstContext() {
+        return head.next == tail ? null : head.next;
+    }
+
+    @Override
+    public EngineHandlerContext lastContext() {
+        return tail.previous == head ? null : tail.previous;
+    }
+
+    //----- Event injection methods
+
+    @Override
+    public ProtonEnginePipeline fireEngineStarting() {
+        ProtonEngineHandlerContext current = head;
+        while (current != tail) {
+            if (engine.state().ordinal() < EngineState.SHUTTING_DOWN.ordinal()) {
+                try {
+                    current.fireEngineStarting();
+                } catch (Throwable error) {
+                    engine.engineFailed(error);
+                    break;
+                }
+                current = current.next;
+            }
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireEngineStateChanged() {
+        try {
+            head.fireEngineStateChanged();
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireFailed(EngineFailedException e) {
+        try {
+            head.fireFailed(e);
+        } catch (Throwable error) {
+            // Ignore errors from handlers as engine is already failed.
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireRead(ProtonBuffer input) {
+        try {
+            tail.fireRead(input);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireRead(HeaderEnvelope header) {
+        try {
+            tail.fireRead(header);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireRead(SASLEnvelope envelope) {
+        try {
+            tail.fireRead(envelope);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireRead(IncomingAMQPEnvelope envelope) {
+        try {
+            tail.fireRead(envelope);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireWrite(HeaderEnvelope envelope) {
+        try {
+            head.fireWrite(envelope);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireWrite(OutgoingAMQPEnvelope envelope) {
+        try {
+            head.fireWrite(envelope);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireWrite(SASLEnvelope envelope) {
+        try {
+            head.fireWrite(envelope);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipeline fireWrite(ProtonBuffer buffer, Runnable ioComplete) {
+        try {
+            head.fireWrite(buffer, ioComplete);
+        } catch (Throwable error) {
+            engine.engineFailed(error);
+            throw error;
+        }
+        return this;
+    }
+
+    //----- Internal implementation
+
+    private ProtonEngineHandlerContext createContext(String name, EngineHandler handler) {
+        return new ProtonEngineHandlerContext(name, engine, handler);
+    }
+
+    //----- Synthetic handler context that bounds the pipeline
+
+    private class EngineHandlerContextReadBoundry extends ProtonEngineHandlerContext {
+
+        public EngineHandlerContextReadBoundry() {
+            super("Read Boundry", engine, new BoundryEngineHandler());
+        }
+
+        @Override
+        public void fireRead(ProtonBuffer buffer) {
+            throw engine.engineFailed(new ProtonException("No handler processed Transport read event."));
+        }
+
+        @Override
+        public void fireRead(HeaderEnvelope header) {
+            throw engine.engineFailed(new ProtonException("No handler processed AMQP Header event."));
+        }
+
+        @Override
+        public void fireRead(SASLEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed SASL performative event."));
+        }
+
+        @Override
+        public void fireRead(IncomingAMQPEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed protocol performative event."));
+        }
+    }
+
+    private class EngineHandlerContextWriteBoundry extends ProtonEngineHandlerContext {
+
+        public EngineHandlerContextWriteBoundry() {
+            super("Write Boundry", engine, new BoundryEngineHandler());
+        }
+
+        @Override
+        public void fireWrite(HeaderEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed write AMQP Header event."));
+        }
+
+        @Override
+        public void fireWrite(OutgoingAMQPEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed write AMQP performative event."));
+        }
+
+        @Override
+        public void fireWrite(SASLEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed write SASL performative event."));
+        }
+
+        @Override
+        public void fireWrite(ProtonBuffer buffer, Runnable ioComplete) {
+            // When not handled in the handler chain the buffer write propagates to the
+            // engine to be handed to any registered output handler.  The engine is then
+            // responsible for error handling if nothing is registered there to handle the
+            // output of frame data.
+            try {
+                engine.dispatchWriteToEventHandler(buffer, ioComplete);
+            } catch (Throwable error) {
+                throw engine.engineFailed(error);
+            }
+        }
+    }
+
+    //----- Default TransportHandler Used at the pipeline boundary
+
+    private class BoundryEngineHandler implements EngineHandler {
+
+        @Override
+        public void engineStarting(EngineHandlerContext context) {
+        }
+
+        @Override
+        public void handleRead(EngineHandlerContext context, ProtonBuffer buffer) {
+            throw engine.engineFailed(new ProtonException("No handler processed Transport read event."));
+        }
+
+        @Override
+        public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+            throw engine.engineFailed(new ProtonException("No handler processed AMQP Header event."));
+        }
+
+        @Override
+        public void handleRead(EngineHandlerContext context, SASLEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed SASL performative read event."));
+        }
+
+        @Override
+        public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed protocol performative read event."));
+        }
+
+        @Override
+        public void handleWrite(EngineHandlerContext context, HeaderEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed write AMQP Header event."));
+        }
+
+        @Override
+        public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed write AMQP performative event."));
+        }
+
+        @Override
+        public void handleWrite(EngineHandlerContext context, SASLEnvelope envelope) {
+            throw engine.engineFailed(new ProtonException("No handler processed write SASL performative event."));
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipelineProxy.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipelineProxy.java
new file mode 100644
index 0000000..983cdc1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipelineProxy.java
@@ -0,0 +1,228 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.Objects;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.EnginePipeline;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineNotWritableException;
+
+/**
+ * Wrapper around the internal {@link ProtonEnginePipeline} used to present a guarded
+ * pipeline to the outside world when the {@link Engine#pipeline()} method is used
+ * to gain access to the pipeline.  The proxy will ensure that any read or write
+ * calls enforce {@link Engine} state such as not started and shutdown.
+ */
+public class ProtonEnginePipelineProxy implements EnginePipeline {
+
+    private final ProtonEnginePipeline pipeline;
+
+    ProtonEnginePipelineProxy(ProtonEnginePipeline pipeline) {
+        Objects.requireNonNull(pipeline, "Must supply a real pipline instance to wrap.");
+        this.pipeline = pipeline;
+    }
+
+    @Override
+    public ProtonEngine engine() {
+        return pipeline.engine();
+    }
+
+    /**
+     * @return the wrapped {@link ProtonEnginePipeline} for testing.
+     */
+    ProtonEnginePipeline pipeline() {
+        return pipeline;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy addFirst(String name, EngineHandler handler) {
+        engine().checkShutdownOrFailed("Cannot add pipeline resources when Engine is shutdown or failed");
+        pipeline.addFirst(name, handler);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy addLast(String name, EngineHandler handler) {
+        engine().checkShutdownOrFailed("Cannot add pipeline resources when Engine is shutdown or failed");
+        pipeline.addLast(name, handler);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy removeFirst() {
+        pipeline.removeFirst();
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy removeLast() {
+        pipeline.removeLast();
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy remove(String name) {
+        pipeline.remove(name);
+        return this;
+    }
+
+    @Override
+    public EnginePipeline remove(EngineHandler handler) {
+        pipeline.remove(handler);
+        return this;
+    }
+
+    @Override
+    public EngineHandler find(String name) {
+        engine().checkShutdownOrFailed("Cannot access pipeline resource when Engine is shutdown or failed");
+        return pipeline.find(name);
+    }
+
+    @Override
+    public EngineHandler first() {
+        engine().checkShutdownOrFailed("Cannot access pipeline resource when Engine is shutdown or failed");
+        return pipeline.first();
+    }
+
+    @Override
+    public EngineHandler last() {
+        engine().checkShutdownOrFailed("Cannot access pipeline resource when Engine is shutdown or failed");
+        return pipeline.last();
+    }
+
+    @Override
+    public EngineHandlerContext firstContext() {
+        engine().checkShutdownOrFailed("Cannot access pipeline resource when Engine is shutdown or failed");
+        return pipeline.firstContext();
+    }
+
+    @Override
+    public EngineHandlerContext lastContext() {
+        engine().checkShutdownOrFailed("Cannot access pipeline resource when Engine is shutdown or failed");
+        return pipeline.lastContext();
+    }
+
+    //----- Event injection methods
+
+    @Override
+    public ProtonEnginePipelineProxy fireEngineStarting() {
+        throw new IllegalAccessError("Cannot trigger starting on Egnine owned Pipeline resource.");
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireEngineStateChanged() {
+        throw new IllegalAccessError("Cannot trigger state changed on Egnine owned Pipeline resource.");
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireFailed(EngineFailedException e) {
+        throw new IllegalAccessError("Cannot trigger failed on Egnine owned Pipeline resource.");
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireRead(ProtonBuffer input) {
+        engine().checkEngineNotStarted("Cannot inject new data into an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot inject new data into an Engine that is shutdown or failed");
+        pipeline.fireRead(input);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireRead(HeaderEnvelope header) {
+        engine().checkEngineNotStarted("Cannot inject new data into an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot inject new data into an Engine that is shutdown or failed");
+        pipeline.fireRead(header);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireRead(SASLEnvelope envelope) {
+        engine().checkEngineNotStarted("Cannot inject new data into an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot inject new data into an Engine that is shutdown or failed");
+        pipeline.fireRead(envelope);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireRead(IncomingAMQPEnvelope envelope) {
+        engine().checkEngineNotStarted("Cannot inject new data into an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot inject new data into an Engine that is shutdown or failed");
+        pipeline.fireRead(envelope);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireWrite(HeaderEnvelope envelope) {
+        engine().checkEngineNotStarted("Cannot write from an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot write form an Engine that is shutdown or failed");
+
+        if (!engine().isWritable()) {
+            throw new EngineNotWritableException("Cannot write through Engine pipeline when Engine is not writable");
+        }
+
+        pipeline.fireWrite(envelope);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireWrite(OutgoingAMQPEnvelope envelope) {
+        engine().checkEngineNotStarted("Cannot write from an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot write form an Engine that is shutdown or failed");
+
+        if (!engine().isWritable()) {
+            throw new EngineNotWritableException("Cannot write through Engine pipeline when Engine is not writable");
+        }
+
+        pipeline.fireWrite(envelope);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireWrite(SASLEnvelope envelope) {
+        engine().checkEngineNotStarted("Cannot write from an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot write form an Engine that is shutdown or failed");
+
+        if (!engine().isWritable()) {
+            throw new EngineNotWritableException("Cannot write through Engine pipeline when Engine is not writable");
+        }
+
+        pipeline.fireWrite(envelope);
+        return this;
+    }
+
+    @Override
+    public ProtonEnginePipelineProxy fireWrite(ProtonBuffer buffer, Runnable ioComplete) {
+        engine().checkEngineNotStarted("Cannot write from an unstarted Engine");
+        engine().checkShutdownOrFailed("Cannot write form an Engine that is shutdown or failed");
+
+        if (!engine().isWritable()) {
+            throw new EngineNotWritableException("Cannot write through Engine pipeline when Engine is not writable");
+        }
+
+        pipeline.fireWrite(buffer, ioComplete);
+        return this;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameDecodingHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameDecodingHandler.java
new file mode 100644
index 0000000..e7c05cf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameDecodingHandler.java
@@ -0,0 +1,409 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.engine.AMQPPerformativeEnvelopePool;
+import org.apache.qpid.protonj2.engine.EmptyEnvelope;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.FrameDecodingException;
+import org.apache.qpid.protonj2.engine.exceptions.MalformedAMQPHeaderException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtonException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.security.SaslOutcome;
+import org.apache.qpid.protonj2.types.security.SaslPerformative;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.Performative;
+
+/**
+ * Handler used to parse incoming frame data input into the engine
+ */
+public class ProtonFrameDecodingHandler implements EngineHandler, SaslPerformative.SaslPerformativeHandler<EngineHandlerContext> {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonFrameDecodingHandler.class);
+
+    public static final byte AMQP_FRAME_TYPE = (byte) 0;
+    public static final byte SASL_FRAME_TYPE = (byte) 1;
+
+    public static final int FRAME_SIZE_BYTES = 4;
+
+    private final AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> framePool = AMQPPerformativeEnvelopePool.incomingEnvelopePool();
+
+    private Decoder decoder;
+    private DecoderState decoderState;
+    private FrameParserStage stage = new HeaderParsingStage();
+    private ProtonEngine engine;
+    private ProtonEngineConfiguration configuration;
+
+    // Parser stages used during the parsing process
+    private final FrameSizeParsingStage frameSizeParser = new FrameSizeParsingStage();
+    private final FrameBufferingStage frameBufferingStage = new FrameBufferingStage();
+    private final FrameBodyParsingStage frameBodyParsingStage = new FrameBodyParsingStage();
+
+    //----- Handler method implementations
+
+    @Override
+    public void handlerAdded(EngineHandlerContext context) {
+        engine = (ProtonEngine) context.engine();
+        configuration = engine.configuration();
+    }
+
+    @Override
+    public void engineFailed(EngineHandlerContext context, EngineFailedException failure) {
+        transitionToErrorStage(failure);
+        context.fireFailed(failure);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, ProtonBuffer buffer) {
+        try {
+            // Parses in-incoming data and emit events for complete frames before returning, caller
+            // should ensure that the input buffer is drained into the engine or stop if the engine
+            // has changed to a non-writable state.
+            while (buffer.isReadable() && engine.isWritable()) {
+                stage.parse(context, buffer);
+            }
+        } catch (FrameDecodingException frameEx) {
+            transitionToErrorStage(frameEx).fireError(context);
+        } catch (ProtonException pex) {
+            transitionToErrorStage(pex).fireError(context);
+        } catch (DecodeException ex) {
+            transitionToErrorStage(new FrameDecodingException(ex.getMessage(), ex)).fireError(context);
+        } catch (Exception error) {
+            transitionToErrorStage(new ProtonException(error.getMessage(), error)).fireError(context);
+        }
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, SASLEnvelope envelope) {
+        envelope.getBody().invoke(this, context);
+        ((ProtonEngineHandlerContext) context).interestMask(ProtonEngineHandlerContext.HANDLER_READS);
+        context.fireRead(envelope);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope envelope) {
+        envelope.invoke(this, context);
+        ((ProtonEngineHandlerContext) context).interestMask(ProtonEngineHandlerContext.HANDLER_READS);
+        context.fireWrite(envelope);
+    }
+
+    //----- SASL Performative Handler to check for change to non-SASL state
+
+    @Override
+    public void handleOutcome(SaslOutcome saslOutcome, EngineHandlerContext context) {
+        // When we have read or written a SASL Outcome the next value to be read
+        // should be an AMQP Header to begin the next phase of the connection.
+        this.stage = new HeaderParsingStage();
+    }
+
+    //---- Methods to transition between stages
+
+    private FrameParserStage transitionToFrameSizeParsingStage() {
+        return stage = frameSizeParser.reset(0);
+    }
+
+    private FrameParserStage transitionToFrameBufferingStage(int length) {
+        return stage = frameBufferingStage.reset(length);
+    }
+
+    private FrameParserStage initializeFrameBodyParsingStage(int length) {
+        return stage = frameBodyParsingStage.reset(length);
+    }
+
+    private ParsingErrorStage transitionToErrorStage(ProtonException error) {
+        if (!(stage instanceof ParsingErrorStage)) {
+            LOG.trace("Frame decoder encounted error: ", error);
+            stage = new ParsingErrorStage(error);
+        }
+
+        return (ParsingErrorStage) stage;
+    }
+
+    //----- Frame Parsing Stage definition
+
+    private interface FrameParserStage {
+
+        /**
+         * Parse the incoming data and provide events to the parent Transport
+         * based on the contents of that data.
+         *
+         * @param context
+         *      The TransportHandlerContext that applies to the current event
+         * @param input
+         *      The ProtonBuffer containing new data to be parsed.
+         */
+        void parse(EngineHandlerContext context, ProtonBuffer input);
+
+        /**
+         * Reset the stage to its defaults for a new cycle of parsing.
+         *
+         * @param length
+         *      The length to use for this part of the parsing operation
+         *
+         * @return a reference to this parsing stage for chaining.
+         */
+        FrameParserStage reset(int length);
+
+    }
+
+    //---- Built in FrameParserStages
+
+    private class HeaderParsingStage implements FrameParserStage {
+
+        private final byte[] headerBytes = new byte[AMQPHeader.HEADER_SIZE_BYTES];
+
+        private int headerByte;
+
+        @Override
+        public void parse(EngineHandlerContext context, ProtonBuffer incoming) {
+            while (incoming.isReadable() && headerByte < AMQPHeader.HEADER_SIZE_BYTES) {
+                byte nextByte = incoming.readByte();
+                try {
+                    AMQPHeader.validateByte(headerByte, nextByte);
+                } catch (IllegalArgumentException iae) {
+                    throw new MalformedAMQPHeaderException(
+                        String.format("Error on validation of header byte %d with value of %d", headerByte, nextByte), iae);
+                }
+                headerBytes[headerByte++] = nextByte;
+            }
+
+            if (headerByte == AMQPHeader.HEADER_SIZE_BYTES) {
+                // Construct a new Header from the read bytes which will validate the contents
+                AMQPHeader header = new AMQPHeader(headerBytes);
+
+                // Transition to parsing the frames if any pipelined into this buffer.
+                transitionToFrameSizeParsingStage();
+
+                // This probably isn't right as this fires to next not current.
+                if (header.isSaslHeader()) {
+                    decoder = CodecFactory.getSaslDecoder();
+                    decoderState = decoder.newDecoderState();
+                    context.fireRead(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+                } else {
+                    decoder = CodecFactory.getDecoder();
+                    decoderState = decoder.newDecoderState();
+                    context.fireRead(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+                }
+            }
+        }
+
+        @Override
+        public HeaderParsingStage reset(int frameSize) {
+            headerByte = 0;
+            return this;
+        }
+    }
+
+    private class FrameSizeParsingStage implements FrameParserStage {
+
+        private int frameSize;
+        private int multiplier = FRAME_SIZE_BYTES;
+
+        @Override
+        public void parse(EngineHandlerContext context, ProtonBuffer input) {
+            while (input.isReadable()) {
+                frameSize |= ((input.readByte() & 0xFF) << --multiplier * Byte.SIZE);
+                if (multiplier == 0) {
+                    break;
+                }
+            }
+
+            if (multiplier == 0) {
+                validateFrameSize();
+
+                int length = frameSize - FRAME_SIZE_BYTES;
+
+                if (input.getReadableBytes() < length) {
+                    transitionToFrameBufferingStage(length);
+                } else {
+                    initializeFrameBodyParsingStage(length);
+                }
+
+                stage.parse(context, input);
+            }
+        }
+
+        private void validateFrameSize() throws FrameDecodingException {
+            if (Integer.compareUnsigned(frameSize, 8) < 0) {
+               throw new FrameDecodingException(String.format(
+                    "specified frame size %d smaller than minimum frame header size 8", frameSize));
+            }
+
+            if (Integer.toUnsignedLong(frameSize) > configuration.getInboundMaxFrameSize()) {
+                throw new FrameDecodingException(String.format(
+                    "specified frame size %s larger than maximum frame size %d",
+                    Integer.toUnsignedString(frameSize), configuration.getInboundMaxFrameSize()));
+            }
+        }
+
+        @Override
+        public FrameSizeParsingStage reset(int frameSize) {
+            multiplier = FRAME_SIZE_BYTES;
+            this.frameSize = frameSize;
+            return this;
+        }
+    }
+
+    private class FrameBufferingStage implements FrameParserStage {
+
+        private ProtonBuffer buffer;
+
+        @Override
+        public void parse(EngineHandlerContext context, ProtonBuffer input) {
+            if (input.getReadableBytes() < buffer.getWritableBytes()) {
+                buffer.writeBytes(input);
+            } else {
+                buffer.writeBytes(input, buffer.getWritableBytes());
+
+                // Now we can consume the buffer frame body.
+                initializeFrameBodyParsingStage(buffer.getReadableBytes());
+                try {
+                    stage.parse(context, buffer);
+                } finally {
+                    buffer = null;
+                }
+            }
+        }
+
+        @Override
+        public FrameBufferingStage reset(int length) {
+            buffer = ProtonByteBufferAllocator.DEFAULT.allocate(length, length);
+            return this;
+        }
+    }
+
+    private class FrameBodyParsingStage implements FrameParserStage {
+
+        private int length;
+
+        @Override
+        public void parse(EngineHandlerContext context, ProtonBuffer input) {
+            int dataOffset = (input.readByte() << 2) & 0x3FF;
+            int frameSize = length + FRAME_SIZE_BYTES;
+
+            validateDataOffset(dataOffset, frameSize);
+
+            int type = input.readByte() & 0xFF;
+            short channel = input.readShort();
+
+            // Skip over the extended header if present (i.e offset > 8)
+            if (dataOffset != 8) {
+                input.setReadIndex(input.getReadIndex() + dataOffset - 8);
+            }
+
+            final int frameBodySize = frameSize - dataOffset;
+
+            ProtonBuffer payload = null;
+            Object val = null;
+
+            if (frameBodySize > 0) {
+                int startReadIndex = input.getReadIndex();
+                val = decoder.readObject(input, decoderState);
+
+                // Copy the payload portion of the incoming bytes for now as the incoming may be
+                // from a wrapped pooled buffer and for now we have no way of retaining or otherwise
+                // ensuring that the buffer remains ours.  Since we might want to store received
+                // data at a client level and decode later we could end up losing the data to reuse
+                // if it was pooled.
+                if (input.isReadable()) {
+                    int payloadSize = frameBodySize - (input.getReadIndex() - startReadIndex);
+                    // Check that the remaining bytes aren't part of another frame.
+                    if (payloadSize > 0) {
+                        payload = configuration.getBufferAllocator().allocate(payloadSize, payloadSize);
+                        payload.writeBytes(input, payloadSize);
+                    }
+                }
+            } else {
+                transitionToFrameSizeParsingStage();
+                context.fireRead(EmptyEnvelope.INSTANCE);
+                return;
+            }
+
+            if (type == AMQP_FRAME_TYPE) {
+                Performative performative = (Performative) val;
+                IncomingAMQPEnvelope frame = framePool.take(performative, channel, payload);
+                transitionToFrameSizeParsingStage();
+                context.fireRead(frame);
+            } else if (type == SASL_FRAME_TYPE) {
+                SaslPerformative performative = (SaslPerformative) val;
+                SASLEnvelope saslFrame = new SASLEnvelope(performative);
+                transitionToFrameSizeParsingStage();
+                // Ensure we process transition from SASL to AMQP header state
+                handleRead(context, saslFrame);
+            } else {
+                throw new FrameDecodingException(String.format("unknown frame type: %d", type));
+            }
+        }
+
+        private void validateDataOffset(int dataOffset, int frameSize) throws FrameDecodingException {
+            if (dataOffset < 8) {
+                throw new FrameDecodingException(String.format(
+                    "specified frame data offset %d smaller than minimum frame header size %d", dataOffset, 8));
+            }
+
+            if (dataOffset > frameSize) {
+                throw new FrameDecodingException(String.format(
+                    "specified frame data offset %d larger than the frame size %d", dataOffset, frameSize));
+            }
+        }
+
+        @Override
+        public FrameBodyParsingStage reset(int length) {
+            this.length = length;
+            return this;
+        }
+    }
+
+    /*
+     * If parsing fails the parser enters the failed state and remains there always throwing the given exception
+     * if additional parsing is requested.
+     */
+    private class ParsingErrorStage implements FrameParserStage {
+
+        private final ProtonException parsingError;
+
+        public ParsingErrorStage(ProtonException parsingError) {
+            this.parsingError = parsingError;
+        }
+
+        public void fireError(EngineHandlerContext context) {
+            throw parsingError;
+        }
+
+        @Override
+        public void parse(EngineHandlerContext context, ProtonBuffer input) {
+            throw new FrameDecodingException(parsingError);
+        }
+
+        @Override
+        public ParsingErrorStage reset(int length) {
+            return this;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameEncodingHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameEncodingHandler.java
new file mode 100644
index 0000000..69a0acc
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameEncodingHandler.java
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.FrameEncodingException;
+import org.apache.qpid.protonj2.types.transport.Performative;
+
+/**
+ * Handler that encodes performatives into properly formed frames for IO
+ */
+public class ProtonFrameEncodingHandler implements EngineHandler {
+
+    public static final byte AMQP_FRAME_TYPE = (byte) 0;
+    public static final byte SASL_FRAME_TYPE = (byte) 1;
+
+    private static final int AMQP_PERFORMATIVE_PAD = 256;
+    private static final int FRAME_HEADER_SIZE = 8;
+    private static final int FRAME_DOFF_SIZE = 2;
+
+    private static final int FRAME_START_BYTE = 0;
+    private static final int FRAME_DOFF_BYTE = 4;
+    private static final int FRAME_TYPE_BYTE = 5;
+    private static final int FRAME_CHANNEL_BYTE = 6;
+
+    private static final byte[] SASL_FRAME_HEADER = new byte[] { 0, 0, 0, 0, FRAME_DOFF_SIZE, SASL_FRAME_TYPE, 0, 0 };
+
+    private static final ProtonBuffer EMPTY_BUFFER = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[0]);
+
+    private final Encoder saslEncoder = CodecFactory.getSaslEncoder();
+    private final EncoderState saslEncoderState = saslEncoder.newEncoderState();
+    private final Encoder amqpEncoder = CodecFactory.getEncoder();
+    private final EncoderState amqpEncoderState = amqpEncoder.newEncoderState();
+
+    private ProtonEngine engine;
+    private ProtonEngineConfiguration configuration;
+
+    @Override
+    public void handlerAdded(EngineHandlerContext context) {
+        engine = (ProtonEngine) context.engine();
+        configuration = engine.configuration();
+
+        ((ProtonEngineHandlerContext) context).interestMask(ProtonEngineHandlerContext.HANDLER_WRITES);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, HeaderEnvelope envelope) {
+        context.fireWrite(envelope.getBody().getBuffer(), null);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope envelope) {
+        ProtonBuffer output = configuration.getBufferAllocator().outputBuffer(AMQP_PERFORMATIVE_PAD, (int) configuration.getOutboundMaxFrameSize());
+
+        output.setWriteIndex(FRAME_HEADER_SIZE);
+        output.setBytes(FRAME_START_BYTE, SASL_FRAME_HEADER);
+
+        try {
+            saslEncoder.writeObject(output, saslEncoderState, envelope.getBody());
+        } catch (EncodeException ex) {
+            throw new FrameEncodingException(ex);
+        } finally {
+            saslEncoderState.reset();
+        }
+
+        context.fireWrite(output.setInt(FRAME_START_BYTE, output.getReadableBytes()), null);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope envelope) {
+        final ProtonBuffer payload = envelope.getPayload() == null ? EMPTY_BUFFER : envelope.getPayload();
+        final int maxFrameSize = (int) configuration.getOutboundMaxFrameSize();
+        final int outputBufferSize = Math.min(maxFrameSize, AMQP_PERFORMATIVE_PAD + payload.getReadableBytes());
+        final ProtonBuffer output = configuration.getBufferAllocator().outputBuffer(outputBufferSize, maxFrameSize);
+
+        writePerformative(output, amqpEncoder, amqpEncoderState, envelope.getBody());
+
+        if (payload.getReadableBytes() > output.getMaxWritableBytes()) {
+            envelope.handlePayloadToLarge();
+
+            writePerformative(output, amqpEncoder, amqpEncoderState, envelope.getBody());
+
+            output.writeBytes(payload, output.getMaxWritableBytes());
+        } else {
+            output.writeBytes(payload);
+        }
+
+        // Now fill in the frame header with the specified information
+        output.setInt(FRAME_START_BYTE, output.getReadableBytes());
+        output.setByte(FRAME_DOFF_BYTE, FRAME_DOFF_SIZE);
+        output.setByte(FRAME_TYPE_BYTE, AMQP_FRAME_TYPE);
+        output.setShort(FRAME_CHANNEL_BYTE, (short) envelope.getChannel());
+
+        context.fireWrite(output, envelope::handleOutgoingFrameWriteComplete);
+    }
+
+    private static void writePerformative(ProtonBuffer target, Encoder encoder, EncoderState state, Performative performative) {
+        target.setWriteIndex(FRAME_HEADER_SIZE);
+
+        try {
+            encoder.writeObject(target, state, performative);
+        } catch (EncodeException ex) {
+            throw new FrameEncodingException(ex);
+        } finally {
+            state.reset();
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameLoggingHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameLoggingHandler.java
new file mode 100644
index 0000000..cf07ea0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameLoggingHandler.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+
+/**
+ * Handler that will log incoming and outgoing Frames
+ */
+public class ProtonFrameLoggingHandler implements EngineHandler {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonFrameLoggingHandler.class);
+
+    private static final String AMQP_IN_PREFIX = "<- AMQP";
+    private static final String AMQP_OUT_PREFIX = "-> AMQP";
+    private static final String SASL_IN_PREFIX = "<- SASL";
+    private static final String SASL_OUT_PREFIX = "-> SASL";
+
+    private static final int PAYLOAD_STRING_LIMIT = 64;
+    private static final String PN_TRACE_FRM = "PN_TRACE_FRM";
+    private static final boolean TRACE_FRM_ENABLED = checkTraceFramesEnabled();
+
+    private boolean traceFrames = TRACE_FRM_ENABLED;
+
+    private static final boolean checkTraceFramesEnabled() {
+        String value = System.getenv(PN_TRACE_FRM);
+        return "true".equalsIgnoreCase(value) || "1".equals(value) || "yes".equalsIgnoreCase(value);
+    }
+
+    void setTraceFrames(boolean traceFrames) {
+        this.traceFrames = traceFrames;
+    }
+
+    boolean isTraceFrames() {
+        return traceFrames;
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+        if (traceFrames) {
+            trace(AMQP_IN_PREFIX, 0, header.getBody(), null);
+        }
+
+        log(AMQP_IN_PREFIX, 0, header.getBody(), null);
+
+        context.fireRead(header);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, SASLEnvelope envelope) {
+        if (traceFrames) {
+            trace(SASL_IN_PREFIX, 0, envelope.getBody(), null);
+        }
+
+        log(SASL_IN_PREFIX, 0, envelope.getBody(), envelope.getPayload());
+
+        context.fireRead(envelope);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope envelope) {
+        if (traceFrames) {
+            trace(AMQP_IN_PREFIX, envelope.getChannel(), envelope.getBody(), envelope.getPayload());
+        }
+
+        if (LOG.isTraceEnabled()) {
+            log(AMQP_IN_PREFIX, envelope.getChannel(), envelope.getBody(), envelope.getPayload());
+        }
+
+        context.fireRead(envelope);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, HeaderEnvelope envelope) {
+        if (traceFrames) {
+            trace(AMQP_OUT_PREFIX, 0, envelope.getBody(), null);
+        }
+
+        log(AMQP_OUT_PREFIX, 0, envelope.getBody(), null);
+
+        context.fireWrite(envelope);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope envelope) {
+        if (traceFrames) {
+            trace(AMQP_OUT_PREFIX, envelope.getChannel(), envelope.getBody(), envelope.getPayload());
+        }
+
+        if (LOG.isTraceEnabled()) {
+            log(AMQP_OUT_PREFIX, envelope.getChannel(), envelope.getBody(), envelope.getPayload());
+        }
+
+        context.fireWrite(envelope);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope envelope) {
+        if (traceFrames) {
+            trace(SASL_OUT_PREFIX, 0, envelope.getBody(), null);
+        }
+
+        log(SASL_OUT_PREFIX, 0, envelope.getBody(), null);
+
+        context.fireWrite(envelope);
+    }
+
+    private static final void log(String prefix, int channel, Object performative, ProtonBuffer payload) {
+        if (payload == null) {
+            LOG.trace("{}: [{}] {}", prefix, channel, performative);
+        } else {
+            LOG.trace("{}: [{}] {} - {}", prefix, performative, StringUtils.toQuotedString(payload, PAYLOAD_STRING_LIMIT, true));
+        }
+    }
+
+    private static final void trace(String prefix, int channel, Object performative, ProtonBuffer payload) {
+        if (payload == null) {
+            System.out.println(String.format("%s: [%d] %s", prefix, channel, performative));
+        } else {
+            System.out.println(String.format("%s: [%d] %s - %s", prefix, channel, performative, StringUtils.toQuotedString(payload, PAYLOAD_STRING_LIMIT, true)));
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonIncomingDelivery.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonIncomingDelivery.java
new file mode 100644
index 0000000..89d0d40
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonIncomingDelivery.java
@@ -0,0 +1,368 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonCompositeBuffer;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+/**
+ * Proton Incoming Delivery implementation
+ */
+public class ProtonIncomingDelivery implements IncomingDelivery {
+
+    private final DeliveryTag deliveryTag;
+    private final ProtonReceiver link;
+    private final long deliveryId;
+
+    private boolean complete;
+    private int messageFormat;
+    private boolean aborted;
+    private int transferCount;
+    private int claimedBytes;
+
+    private DeliveryState defaultDeliveryState;
+
+    private DeliveryState localState;
+    private boolean locallySettled;
+
+    private DeliveryState remoteState;
+    private boolean remotelySettled;
+
+    private ProtonBuffer payload;
+    private ProtonCompositeBuffer aggregate;
+
+    private ProtonAttachments attachments;
+    private Object linkedResource;
+
+    private EventHandler<IncomingDelivery> deliveryReadEventHandler = null;
+    private EventHandler<IncomingDelivery> deliveryAbortedEventHandler = null;
+    private EventHandler<IncomingDelivery> deliveryUpdatedEventHandler = null;
+
+    /**
+     * @param link
+     *      The link that this delivery is associated with
+     * @param deliveryId
+     *      The Delivery Id that is assigned to this delivery.
+     * @param deliveryTag
+     *      The delivery tag assigned to this delivery
+     */
+    public ProtonIncomingDelivery(ProtonReceiver link, long deliveryId, DeliveryTag deliveryTag) {
+        this.deliveryId = deliveryId;
+        this.deliveryTag = deliveryTag;
+        this.link = link;
+    }
+
+    @Override
+    public ProtonReceiver getLink() {
+        return link;
+    }
+
+    @Override
+    public ProtonAttachments getAttachments() {
+        return attachments == null ? attachments = new ProtonAttachments() : attachments;
+    }
+
+    @Override
+    public ProtonIncomingDelivery setLinkedResource(Object resource) {
+        this.linkedResource = resource;
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T getLinkedResource() {
+        return (T) linkedResource;
+    }
+
+    @Override
+    public <T> T getLinkedResource(Class<T> typeClass) {
+        return typeClass.cast(linkedResource);
+    }
+
+    @Override
+    public DeliveryTag getTag() {
+        return deliveryTag;
+    }
+
+    @Override
+    public DeliveryState getState() {
+        return localState;
+    }
+
+    @Override
+    public DeliveryState getRemoteState() {
+        return remoteState;
+    }
+
+    @Override
+    public int getMessageFormat() {
+        return messageFormat;
+    }
+
+    ProtonIncomingDelivery setMessageFormat(int messageFormat) {
+        this.messageFormat = messageFormat;
+        return this;
+    }
+
+    @Override
+    public boolean isPartial() {
+        return !complete || aborted;
+    }
+
+    @Override
+    public boolean isAborted() {
+        return aborted;
+    }
+
+    @Override
+    public boolean isSettled() {
+        return locallySettled;
+    }
+
+    @Override
+    public boolean isRemotelySettled() {
+        return remotelySettled;
+    }
+
+    @Override
+    public ProtonIncomingDelivery setDefaultDeliveryState(DeliveryState state) {
+        this.defaultDeliveryState = state;
+        return this;
+    }
+
+    @Override
+    public DeliveryState getDefaultDeliveryState() {
+        return defaultDeliveryState;
+    }
+
+    @Override
+    public IncomingDelivery disposition(DeliveryState state) {
+        return disposition(state, false);
+    }
+
+    @Override
+    public IncomingDelivery disposition(DeliveryState state, boolean settle) {
+        if (locallySettled) {
+            if ((localState != null && !localState.equals(state)) || localState != state) {
+                throw new IllegalStateException("Cannot update disposition on an already settled Delivery");
+            } else {
+                return this;
+            }
+        }
+
+        this.locallySettled = settle;
+        this.localState = state;
+        this.link.disposition(this);
+
+        return this;
+    }
+
+    @Override
+    public IncomingDelivery settle() {
+        return disposition(localState, true);
+    }
+
+    //----- Payload access
+
+    @Override
+    public int available() {
+        return payload == null ? 0 : payload.getReadableBytes();
+    }
+
+    @Override
+    public ProtonBuffer readAll() {
+        ProtonBuffer result = null;
+        if (payload != null) {
+            int bytesRead = claimedBytes -= payload.getReadableBytes();
+            result = payload;
+            payload = null;
+            aggregate = null;
+            if (bytesRead < 0) {
+                claimedBytes = 0;
+                link.deliveryRead(this, -bytesRead);
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public ProtonIncomingDelivery readBytes(ProtonBuffer buffer) {
+        if (payload != null) {
+            int bytesRead = payload.getReadableBytes();
+            payload.readBytes(buffer);
+            bytesRead -= payload.getReadableBytes();
+            if (!payload.isReadable()) {
+                payload = null;
+                aggregate = null;
+            }
+
+            bytesRead = claimedBytes -= bytesRead;
+            if (bytesRead < 0) {
+                claimedBytes = 0;
+                link.deliveryRead(this, -bytesRead);
+            }
+        }
+        return this;
+    }
+
+    @Override
+    public ProtonIncomingDelivery readBytes(byte[] array, int offset, int length) {
+        if (payload != null) {
+            int bytesRead = payload.getReadableBytes();
+            payload.readBytes(array, offset, length);
+            bytesRead -= payload.getReadableBytes();
+            if (!payload.isReadable()) {
+                payload = null;
+                aggregate = null;
+            }
+
+            bytesRead = claimedBytes -= bytesRead;
+            if (bytesRead < 0) {
+                claimedBytes = 0;
+                link.deliveryRead(this, -bytesRead);
+            }
+        }
+        return this;
+    }
+
+    @Override
+    public IncomingDelivery claimAvailableBytes() {
+        long available = available();
+
+        if (available > 0) {
+            long unclaimed = available - claimedBytes;
+            if (unclaimed > 0) {
+                claimedBytes += unclaimed;
+                link.deliveryRead(this, (int) unclaimed);
+            }
+        }
+
+        return this;
+    }
+
+    //----- Incoming Delivery event handlers
+
+    @Override
+    public ProtonIncomingDelivery deliveryReadHandler(EventHandler<IncomingDelivery> handler) {
+        this.deliveryReadEventHandler = handler;
+        return this;
+    }
+
+    EventHandler<IncomingDelivery> deliveryReadHandler() {
+        return deliveryReadEventHandler;
+    }
+
+    @Override
+    public ProtonIncomingDelivery deliveryAbortedHandler(EventHandler<IncomingDelivery> handler) {
+        this.deliveryAbortedEventHandler = handler;
+        return this;
+    }
+
+    EventHandler<IncomingDelivery> deliveryAbortedHandler() {
+        return deliveryAbortedEventHandler;
+    }
+
+    @Override
+    public ProtonIncomingDelivery deliveryStateUpdatedHandler(EventHandler<IncomingDelivery> handler) {
+        this.deliveryUpdatedEventHandler = handler;
+        return this;
+    }
+
+    EventHandler<IncomingDelivery> deliveryStateUpdatedHandler() {
+        return deliveryUpdatedEventHandler;
+    }
+
+    //----- Internal methods to manage the Delivery
+
+    @Override
+    public int getTransferCount() {
+        return transferCount;
+    }
+
+    boolean isFirstTransfer() {
+        return transferCount <= 1;
+    }
+
+    long getDeliveryId() {
+        return deliveryId;
+    }
+
+    ProtonIncomingDelivery aborted() {
+        aborted = true;
+
+        if (payload != null) {
+            final int bytesRead = payload.getReadableBytes();
+
+            payload = null;
+            aggregate = null;
+
+            // Ensure Session no longer records these in the window metrics
+            link.deliveryRead(this, bytesRead);
+        }
+
+        return this;
+    }
+
+    ProtonIncomingDelivery completed() {
+        this.complete = true;
+        return this;
+    }
+
+    ProtonIncomingDelivery remotelySettled() {
+        this.remotelySettled = true;
+        return this;
+    }
+
+    ProtonIncomingDelivery remoteState(DeliveryState remoteState) {
+        this.remoteState = remoteState;
+        return this;
+    }
+
+    ProtonIncomingDelivery locallySettled() {
+        this.locallySettled = true;
+        return this;
+    }
+
+    ProtonIncomingDelivery localState(DeliveryState localState) {
+        this.localState = localState;
+        return this;
+    }
+
+    ProtonIncomingDelivery appendTransferPayload(ProtonBuffer buffer) {
+        transferCount++;
+
+        if (payload == null) {
+            payload = buffer;
+        } else if (aggregate != null) {
+            aggregate.append(buffer);
+        } else {
+            final ProtonBuffer previous = payload;
+
+            payload = aggregate = new ProtonCompositeBuffer();
+
+            aggregate.append(previous);
+            aggregate.append(buffer);
+        }
+
+        return this;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonLink.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonLink.java
new file mode 100644
index 0000000..96c4ddf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonLink.java
@@ -0,0 +1,816 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.Link;
+import org.apache.qpid.protonj2.engine.LinkState;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.EngineShutdownException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Common base for Proton Senders and Receivers.
+ *
+ * @param <L> the type of link, {@link Sender} or {@link Receiver}.
+ */
+public abstract class ProtonLink<L extends Link<L>> extends ProtonEndpoint<L> implements Link<L> {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonLink.class);
+
+    private enum LinkOperabilityState {
+        OK,
+        LINK_REMOTELY_DETACHED,
+        LINK_LOCALLY_DETACHED,
+        LINK_REMOTELY_CLOSED,
+        LINK_LOCALLY_CLOSED,
+        SESSION_REMOTELY_CLOSED,
+        SESSION_LOCALLY_CLOSED,
+        CONNECTION_REMOTELY_CLOSED,
+        CONNECTION_LOCALLY_CLOSED,
+        ENGINE_SHUTDOWN
+    }
+
+    protected final ProtonConnection connection;
+    protected final ProtonSession session;
+
+    protected final Attach localAttach = new Attach();
+    protected Attach remoteAttach;
+
+    private boolean localAttachSent;
+    private boolean localDetachSent;
+
+    private final ProtonLinkCreditState creditState;
+
+    private LinkOperabilityState operability = LinkOperabilityState.OK;
+    private LinkState localState = LinkState.IDLE;
+    private LinkState remoteState = LinkState.IDLE;
+
+    private EventHandler<L> localDetachHandler;
+    private EventHandler<L> remoteDetachHandler;
+
+    private EventHandler<L> parentEndpointClosedEventHandler;
+
+    /**
+     * Create a new link instance with the given parent session.
+     *
+     * @param session
+     *      The {@link Session} that this link resides within.
+     * @param name
+     *      The name assigned to this {@link Link}
+     * @param creditState
+     *      The link credit state used to track credit for the link.
+     */
+    protected ProtonLink(ProtonSession session, String name, ProtonLinkCreditState creditState) {
+        super(session.getEngine());
+
+        this.session = session;
+        this.connection = session.getConnection();
+        this.creditState = creditState;
+        this.localAttach.setName(name);
+        this.localAttach.setRole(getRole());
+    }
+
+    @Override
+    public ProtonConnection getConnection() {
+        return connection;
+    }
+
+    @Override
+    public ProtonSession getSession() {
+        return session;
+    }
+
+    @Override
+    public ProtonSession getParent() {
+        return session;
+    }
+
+    @Override
+    public String getName() {
+        return localAttach.getName();
+    }
+
+    @Override
+    public boolean isSender() {
+        return getRole() == Role.SENDER;
+    }
+
+    @Override
+    public boolean isReceiver() {
+        return getRole() == Role.RECEIVER;
+    }
+
+    @Override
+    protected abstract L self();
+
+    long getHandle() {
+        return localAttach.getHandle();
+    }
+
+    @Override
+    public LinkState getState() {
+        return localState;
+    }
+
+    @Override
+    public LinkState getRemoteState() {
+        return remoteState;
+    }
+
+    @Override
+    public L open() {
+        if (getState() == LinkState.IDLE) {
+            checkLinkOperable("Cannot open Link");
+            localState = LinkState.ACTIVE;
+            long localHandle = session.findFreeLocalHandle(this);
+            localAttach.setHandle(localHandle);
+            transitionedToLocallyOpened();
+            try {
+                trySyncLocalStateWithRemote();
+            } finally {
+                fireLocalOpen();
+            }
+        }
+
+        return self();
+    }
+
+    @Override
+    public L detach() {
+        if (getState() == LinkState.ACTIVE) {
+            localState = LinkState.DETACHED;
+            if (operability.ordinal() < LinkOperabilityState.LINK_LOCALLY_DETACHED.ordinal()) {
+                operability = LinkOperabilityState.LINK_LOCALLY_DETACHED;
+            }
+            getCreditState().clearCredit();
+            transitionedToLocallyDetached();
+            try {
+                engine.checkFailed("Closed called on already failed connection");
+                trySyncLocalStateWithRemote();
+            } finally {
+                fireLocalDetach();
+            }
+        }
+
+        return self();
+    }
+
+    @Override
+    public L close() {
+        if (getState() == LinkState.ACTIVE) {
+            localState = LinkState.CLOSED;
+            if (operability.ordinal() < LinkOperabilityState.LINK_LOCALLY_CLOSED.ordinal()) {
+                operability = LinkOperabilityState.LINK_LOCALLY_CLOSED;
+            }
+            getCreditState().clearCredit();
+            transitionedToLocallyClosed();
+            try {
+                engine.checkFailed("Detached called on already failed connection");
+                trySyncLocalStateWithRemote();
+            } finally {
+                fireLocalClose();
+            }
+        }
+
+        return self();
+    }
+
+    @Override
+    public L setSenderSettleMode(SenderSettleMode senderSettleMode) {
+        checkNotOpened("Cannot set Sender settlement mode on already opened Link");
+        localAttach.setSenderSettleMode(senderSettleMode);
+        return self();
+    }
+
+    @Override
+    public SenderSettleMode getSenderSettleMode() {
+        return localAttach.getSenderSettleMode();
+    }
+
+    @Override
+    public L setReceiverSettleMode(ReceiverSettleMode receiverSettleMode) {
+        checkNotOpened("Cannot set Receiver settlement mode already opened Link");
+        localAttach.setReceiverSettleMode(receiverSettleMode);
+        return self();
+    }
+
+    @Override
+    public ReceiverSettleMode getReceiverSettleMode() {
+        return localAttach.getReceiverSettleMode();
+    }
+
+    @Override
+    public L setSource(Source source) {
+        checkNotOpened("Cannot set Source on already opened Link");
+        localAttach.setSource(source);
+        return self();
+    }
+
+    @Override
+    public Source getSource() {
+        return localAttach.getSource();
+    }
+
+    @Override
+    public L setTarget(Target target) {
+        checkNotOpened("Cannot set Target on already opened Link");
+        localAttach.setTarget(target);
+        return self();
+    }
+
+
+    @Override
+    public L setTarget(Coordinator coordinatior) throws IllegalStateException {
+        checkNotOpened("Cannot set Coordinator on already opened Link");
+        localAttach.setTarget(coordinatior);
+        return self();
+    }
+
+    @Override
+    public <T extends Terminus> T getTarget() {
+        return localAttach.getTarget();
+    }
+
+    @Override
+    public L setProperties(Map<Symbol, Object> properties) {
+        checkNotOpened("Cannot set Properties on already opened Link");
+
+        if (properties != null) {
+            localAttach.setProperties(new LinkedHashMap<>(properties));
+        } else {
+            localAttach.setProperties(properties);
+        }
+
+        return self();
+    }
+
+    @Override
+    public Map<Symbol, Object> getProperties() {
+        if (localAttach.getProperties() != null) {
+            return Collections.unmodifiableMap(localAttach.getProperties());
+        }
+
+        return null;
+    }
+
+    @Override
+    public L setOfferedCapabilities(Symbol... capabilities) {
+        checkNotOpened("Cannot set Offered Capabilities on already opened Link");
+
+        if (capabilities != null) {
+            localAttach.setOfferedCapabilities(Arrays.copyOf(capabilities, capabilities.length));
+        } else {
+            localAttach.setOfferedCapabilities(capabilities);
+        }
+
+        return self();
+    }
+
+    @Override
+    public Symbol[] getOfferedCapabilities() {
+        if (localAttach.getOfferedCapabilities() != null) {
+            return Arrays.copyOf(localAttach.getOfferedCapabilities(), localAttach.getOfferedCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public L setDesiredCapabilities(Symbol... capabilities) {
+        checkNotOpened("Cannot set Desired Capabilities on already opened Link");
+
+        if (capabilities != null) {
+            localAttach.setDesiredCapabilities(Arrays.copyOf(capabilities, capabilities.length));
+        } else {
+            localAttach.setDesiredCapabilities(capabilities);
+        }
+
+        return self();
+    }
+
+    @Override
+    public Symbol[] getDesiredCapabilities() {
+        if (localAttach.getDesiredCapabilities() != null) {
+            return Arrays.copyOf(localAttach.getDesiredCapabilities(), localAttach.getDesiredCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public L setMaxMessageSize(UnsignedLong maxMessageSize) {
+        checkNotOpened("Cannot set Max Message Size on already opened Link");
+        localAttach.setMaxMessageSize(maxMessageSize);
+        return self();
+    }
+
+    @Override
+    public UnsignedLong getMaxMessageSize() {
+        return localAttach.getMaxMessageSize();
+    }
+
+    @Override
+    public boolean isLocallyOpen() {
+        return getState() == LinkState.ACTIVE;
+    }
+
+    @Override
+    public boolean isLocallyClosed() {
+        return getState() == LinkState.CLOSED;
+    }
+
+    @Override
+    public boolean isLocallyDetached() {
+        return getState() == LinkState.DETACHED;
+    }
+
+    @Override
+    public boolean isLocallyClosedOrDetached() {
+        return getState().ordinal() > LinkState.ACTIVE.ordinal();
+    }
+
+    @Override
+    public boolean isRemotelyOpen() {
+        return getRemoteState() == LinkState.ACTIVE;
+    }
+
+    @Override
+    public boolean isRemotelyClosed() {
+        return getRemoteState() == LinkState.CLOSED;
+    }
+
+    @Override
+    public boolean isRemotelyDetached() {
+        return getRemoteState() == LinkState.DETACHED;
+    }
+
+    @Override
+    public boolean isRemotelyClosedOrDetached() {
+        return getRemoteState().ordinal() > LinkState.ACTIVE.ordinal();
+    }
+
+    @Override
+    public SenderSettleMode getRemoteSenderSettleMode() {
+        if (remoteAttach != null) {
+            return remoteAttach.getSenderSettleMode();
+        }
+
+        return null;
+    }
+
+    @Override
+    public ReceiverSettleMode getRemoteReceiverSettleMode() {
+        if (remoteAttach != null) {
+            return remoteAttach.getReceiverSettleMode();
+        }
+
+        return null;
+    }
+
+    @Override
+    public Source getRemoteSource() {
+        if (remoteAttach != null && remoteAttach.getSource() != null) {
+            return remoteAttach.getSource().copy();
+        }
+
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends Terminus> T getRemoteTarget() {
+        if (remoteAttach != null && remoteAttach.getTarget() != null) {
+            return (T) remoteAttach.getTarget().copy();
+        }
+
+        return null;
+    }
+
+    @Override
+    public Symbol[] getRemoteOfferedCapabilities() {
+        if (remoteAttach != null && remoteAttach.getOfferedCapabilities() != null) {
+            return Arrays.copyOf(remoteAttach.getOfferedCapabilities(), remoteAttach.getOfferedCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Symbol[] getRemoteDesiredCapabilities() {
+        if (remoteAttach != null && remoteAttach.getDesiredCapabilities() != null) {
+            return Arrays.copyOf(remoteAttach.getDesiredCapabilities(), remoteAttach.getDesiredCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Map<Symbol, Object> getRemoteProperties() {
+        if (remoteAttach != null && remoteAttach.getProperties() != null) {
+            return Collections.unmodifiableMap(remoteAttach.getProperties());
+        }
+
+        return null;
+    }
+
+    @Override
+    public UnsignedLong getRemoteMaxMessageSize() {
+        if (remoteAttach != null && remoteAttach.getMaxMessageSize() != null) {
+            return remoteAttach.getMaxMessageSize();
+        }
+
+        return null;
+    }
+
+    //----- Event registration methods
+
+    @Override
+    public L localDetachHandler(EventHandler<L> localDetachHandler) {
+        this.localDetachHandler = localDetachHandler;
+        return self();
+    }
+
+    EventHandler<L> localDetachHandler() {
+        return localDetachHandler;
+    }
+
+    L fireLocalDetach() {
+        if (localDetachHandler != null) {
+            localDetachHandler.handle(self());
+        } else {
+            fireLocalClose();
+        }
+
+        return self();
+    }
+
+    @Override
+    public L detachHandler(EventHandler<L> remoteDetachHandler) {
+        this.remoteDetachHandler = remoteDetachHandler;
+        return self();
+    }
+
+    EventHandler<L> detachHandler() {
+        return remoteDetachHandler;
+    }
+
+    L fireRemoteDetach() {
+        if (remoteDetachHandler != null) {
+            remoteDetachHandler.handle(self());
+        } else {
+            fireRemoteClose();
+        }
+
+        return self();
+    }
+
+    @Override
+    public L parentEndpointClosedHandler(EventHandler<L> handler) {
+        this.parentEndpointClosedEventHandler = handler;
+        return self();
+    }
+
+    EventHandler<L> parentEndpointClosedHandler() {
+        return parentEndpointClosedEventHandler;
+    }
+
+    L fireParentEndpointClosed() {
+        if (parentEndpointClosedEventHandler != null && isLocallyOpen()) {
+            parentEndpointClosedEventHandler.handle(self());
+        }
+
+        return self();
+    }
+
+    //----- Link state change handlers that can be overridden by specific link implementations
+
+    protected void transitionedToLocallyOpened() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionedToLocallyDetached() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionedToLocallyClosed() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionToRemotelyOpenedState() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionToRemotelyDetached() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionToRemotelyCosed() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionToParentLocallyClosed() {
+        // Nothing currently updated on this state change.
+    }
+
+    protected void transitionToParentRemotelyClosed() {
+        // Nothing currently updated on this state change.
+    }
+
+    //----- Process local events from the parent session and connection
+
+    final void handleSessionLocallyClosed(ProtonSession session) {
+        if (isSender()) {
+            getCreditState().clearCredit();
+        }
+
+        if (operability.ordinal() < LinkOperabilityState.SESSION_LOCALLY_CLOSED.ordinal()) {
+            operability = LinkOperabilityState.SESSION_LOCALLY_CLOSED;
+            transitionToParentLocallyClosed();
+            fireParentEndpointClosed();
+        }
+    }
+
+    final void handleSessionRemotelyClosed(ProtonSession session) {
+        if (isSender()) {
+            getCreditState().clearCredit();
+        }
+
+        if (operability.ordinal() < LinkOperabilityState.SESSION_REMOTELY_CLOSED.ordinal()) {
+            operability = LinkOperabilityState.SESSION_REMOTELY_CLOSED;
+            transitionToParentRemotelyClosed();
+        }
+    }
+
+    final void handleConnectionLocallyClosed(ProtonConnection connection) {
+        if (isSender()) {
+            getCreditState().clearCredit();
+        }
+
+        if (operability.ordinal() < LinkOperabilityState.CONNECTION_LOCALLY_CLOSED.ordinal()) {
+            operability = LinkOperabilityState.CONNECTION_LOCALLY_CLOSED;
+            transitionToParentLocallyClosed();
+            fireParentEndpointClosed();
+        }
+    }
+
+    final void handleConnectionRemotelyClosed(ProtonConnection connection) {
+        if (isSender()) {
+            getCreditState().clearCredit();
+        }
+
+        if (operability.ordinal() < LinkOperabilityState.CONNECTION_REMOTELY_CLOSED.ordinal()) {
+            operability = LinkOperabilityState.CONNECTION_REMOTELY_CLOSED;
+            transitionToParentRemotelyClosed();
+        }
+    }
+
+    final void handleEngineShutdown(ProtonEngine protonEngine) {
+        if (isSender()) {
+            getCreditState().clearCredit();
+        }
+
+        if (operability.ordinal() < LinkOperabilityState.ENGINE_SHUTDOWN.ordinal()) {
+            operability = LinkOperabilityState.ENGINE_SHUTDOWN;
+        }
+
+        try {
+            fireEngineShutdown();
+        } catch (Throwable ignore) {}
+    }
+
+    //----- Handle incoming performatives
+
+    final void remoteAttach(Attach attach) {
+        LOG.trace("Link:{} Received remote Attach:{}", self(), attach);
+
+        remoteAttach = attach;
+        remoteState = LinkState.ACTIVE;
+        handleRemoteAttach(attach);
+        transitionToRemotelyOpenedState();
+
+        if (openHandler() != null) {
+            fireRemoteOpen();
+        } else {
+            if (getRole() == Role.RECEIVER) {
+                if (attach.getTarget() instanceof Coordinator) {
+                    if (session.transactionManagerOpenHandler() != null) {
+                        session.transactionManagerOpenHandler().handle(new ProtonTransactionManager((ProtonReceiver) this));
+                    } else if (connection.transactionManagerOpenHandler() != null) {
+                        connection.transactionManagerOpenHandler().handle(new ProtonTransactionManager((ProtonReceiver) this));
+                    }
+                }
+
+                if (session.receiverOpenEventHandler() != null) {
+                    session.receiverOpenEventHandler().handle((Receiver) this);
+                } else if (connection.receiverOpenEventHandler() != null) {
+                    connection.receiverOpenEventHandler().handle((Receiver) this);
+                } else {
+                    LOG.info("Receiver opened but no event handler registered to inform: {}", this);
+                }
+            } else {
+                if (session.senderOpenEventHandler() != null) {
+                    session.senderOpenEventHandler().handle((Sender) this);
+                } else if (connection.senderOpenEventHandler() != null) {
+                    connection.senderOpenEventHandler().handle((Sender) this);
+                } else {
+                    LOG.info("Sender opened but no event handler registered to inform: {}", this);
+                }
+            }
+        }
+    }
+
+    final ProtonLink<?> remoteDetach(Detach detach) {
+        LOG.trace("Link:{} Received remote Detach:{}", self(), detach);
+        setRemoteCondition(detach.getError());
+        if (isSender()) {
+            getCreditState().clearCredit();
+        }
+
+        handleRemoteDetach(detach);
+
+        if (detach.getClosed()) {
+            remoteState = LinkState.CLOSED;
+            operability = LinkOperabilityState.LINK_REMOTELY_CLOSED;
+            transitionToRemotelyCosed();
+            fireRemoteClose();
+        } else {
+            remoteState = LinkState.DETACHED;
+            operability = LinkOperabilityState.LINK_REMOTELY_DETACHED;
+            transitionToRemotelyDetached();
+            fireRemoteDetach();
+        }
+
+        return this;
+    }
+
+    final ProtonIncomingDelivery remoteTransfer(Transfer transfer, ProtonBuffer payload) {
+        LOG.trace("Link:{} Received new Transfer:{}", self(), transfer);
+        return handleRemoteTransfer(transfer, payload);
+    }
+
+    final L remoteFlow(Flow flow) {
+        LOG.trace("Link:{} Received new Flow:{}", self(), flow);
+        return handleRemoteFlow(flow);
+    }
+
+    final L remoteDisposition(Disposition disposition, ProtonOutgoingDelivery delivery) {
+        LOG.trace("Link:{} Received remote disposition:{} for sent delivery:{}", self(), disposition, delivery);
+        return handleRemoteDisposition(disposition, delivery);
+    }
+
+    final L remoteDisposition(Disposition disposition, ProtonIncomingDelivery delivery) {
+        LOG.trace("Link:{} Received remote disposition:{} for received delivery:{}", self(), disposition, delivery);
+        return handleRemoteDisposition(disposition, delivery);
+    }
+
+    //----- Abstract methods required for specialization of the link type
+
+    protected abstract L handleRemoteAttach(Attach attach);
+
+    protected abstract L handleRemoteDetach(Detach detach);
+
+    protected abstract L handleRemoteFlow(Flow flow);
+
+    protected abstract L handleRemoteDisposition(Disposition disposition, ProtonOutgoingDelivery delivery);
+
+    protected abstract L handleRemoteDisposition(Disposition disposition, ProtonIncomingDelivery delivery);
+
+    protected abstract ProtonIncomingDelivery handleRemoteTransfer(Transfer transfer, ProtonBuffer payload);
+
+    protected abstract L decorateOutgoingFlow(Flow flow);
+
+    //----- Internal methods
+
+    ProtonLinkCreditState getCreditState() {
+        return creditState;
+    }
+
+    boolean wasLocalAttachSent() {
+        return localAttachSent;
+    }
+
+    boolean wasLocalDetachSent() {
+        return localDetachSent;
+    }
+
+    void trySyncLocalStateWithRemote() {
+        switch (getState()) {
+            case IDLE:
+                return;
+            case ACTIVE:
+                trySendLocalAttach();
+                break;
+            case CLOSED:
+            case DETACHED:
+                trySendLocalAttach();
+                trySendLocalDetach(isLocallyClosed());
+                break;
+            default:
+                throw new IllegalStateException("Link is in unknown state and cannot proceed");
+        }
+    }
+
+    private void trySendLocalAttach() {
+        if (!wasLocalAttachSent()) {
+            if ((session.isLocallyOpen() && session.wasLocalBeginSent()) &&
+                (connection.isLocallyOpen() && connection.wasLocalOpenSent())) {
+
+                session.getEngine().fireWrite(localAttach, session.getLocalChannel());
+                localAttachSent = true;
+
+                if (isLocallyOpen() && isReceiver() && getCreditState().hasCredit()) {
+                    session.writeFlow(this);
+                }
+            }
+        }
+    }
+
+    private void trySendLocalDetach(boolean closed) {
+        if (!wasLocalDetachSent()) {
+            if ((session.isLocallyOpen() && session.wasLocalBeginSent()) &&
+                (connection.isLocallyOpen() && connection.wasLocalOpenSent()) && !engine.isShutdown()) {
+
+                Detach detach = new Detach();
+                detach.setHandle(localAttach.getHandle());
+                detach.setClosed(closed);
+                detach.setError(getCondition());
+
+                session.getEngine().fireWrite(detach, session.getLocalChannel());
+                session.freeLink(this);
+                localDetachSent = true;
+            }
+        }
+    }
+
+    protected void checkLinkOperable(String failurePrefix) {
+        switch (operability) {
+            case OK:
+                break;
+            case ENGINE_SHUTDOWN:
+                throw new EngineShutdownException(failurePrefix + ": " + operability.toString());
+            default:
+                throw new IllegalStateException(failurePrefix + ": " + operability.toString());
+        }
+    }
+
+    protected boolean areDeliveriesStillActive() {
+        switch (operability) {
+            case OK:
+            case LINK_REMOTELY_DETACHED:
+            case LINK_LOCALLY_DETACHED:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    protected void checkNotOpened(String errorMessage) {
+        if (localState.ordinal() > LinkState.IDLE.ordinal()) {
+            throw new IllegalStateException(errorMessage);
+        }
+    }
+
+    protected void checkNotClosed(String errorMessage) {
+        if (localState.ordinal() > LinkState.ACTIVE.ordinal()) {
+            throw new IllegalStateException(errorMessage);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonLinkCreditState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonLinkCreditState.java
new file mode 100644
index 0000000..e60b14a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonLinkCreditState.java
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.engine.LinkCreditState;
+import org.apache.qpid.protonj2.types.transport.Flow;
+
+/**
+ * Holds the current credit state for a given link.
+ */
+public class ProtonLinkCreditState implements LinkCreditState {
+
+    private int credit;
+    private int deliveryCount;
+
+    private boolean drain;
+    private boolean echo;
+
+    private boolean deliveryCountInitalised;
+
+    @SuppressWarnings("unused")
+    private long remoteDeliveryCount;
+    @SuppressWarnings("unused")
+    private long remoteLinkCredit;
+
+    public ProtonLinkCreditState() {}
+
+    public ProtonLinkCreditState(int deliveryCount) {
+        initialiseDeliveryCount(deliveryCount);
+    }
+
+    @Override
+    public int getCredit() {
+        return credit;
+    }
+
+    @Override
+    public int getDeliveryCount() {
+        return deliveryCount;
+    }
+
+    @Override
+    public boolean isDrain() {
+        return drain;
+    }
+
+    @Override
+    public boolean isEcho() {
+        return echo;
+    }
+
+    //----- Internal API for managing credit state
+
+    boolean hasCredit() {
+        return Integer.compareUnsigned(credit, 0) > 0;
+    }
+
+    void clearDrain() {
+        drain = false;
+    }
+
+    void clearEcho() {
+        echo = false;
+    }
+
+    void clearCredit() {
+        credit = 0;
+    }
+
+    void incrementCredit(int credit) {
+        this.credit += credit;
+    }
+
+    void decrementCredit() {
+        credit = credit == 0 ? 0 : credit - 1;
+    }
+
+    int incrementDeliveryCount() {
+        return deliveryCount++;
+    }
+
+    int incrementDeliveryCount(int amount) {
+        return deliveryCount += amount;
+    }
+
+    int decrementDeliveryCount() {
+        return deliveryCount--;
+    }
+
+    boolean isDeliveryCountInitalised() {
+        return deliveryCountInitalised;
+    }
+
+    void initialiseDeliveryCount(int deliveryCount) {
+        this.deliveryCount = deliveryCount;
+        deliveryCountInitalised = true;
+    }
+
+    public void updateCredit(int effectiveCredit) {
+        // TODO: change credit to a long, or ensure inc/decrements above work fully if it has wrapped.
+        this.credit = effectiveCredit;
+    }
+
+    public void updateDeliveryCount(int deliveryCount) {
+        // TODO: change deliveryCount to a long, or fix uses to account for it being a wrapping int
+        this.deliveryCount = deliveryCount;
+    }
+
+    void remoteFlow(Flow flow) {
+        remoteDeliveryCount = flow.getDeliveryCount();
+        remoteLinkCredit = flow.getLinkCredit();
+        echo = flow.getEcho();
+        drain = flow.getDrain();
+    }
+
+    /**
+     * Creates a snapshot of the current credit state, a subclass should implement this
+     * method and provide an appropriately populated snapshot of the current state.
+     *
+     * @return a snapshot of the current credit state.
+     */
+    LinkCreditState snapshot() {
+        return new UnmodifiableLinkCreditState(credit, deliveryCount, drain, echo);
+    }
+
+    //----- Provide an immutable view type for protection
+
+    private static class UnmodifiableLinkCreditState implements LinkCreditState {
+
+        private final int credit;
+        private final int deliveryCount;
+        private final boolean drain;
+        private final boolean echo;
+
+        public UnmodifiableLinkCreditState(int credit, int deliveryCount, boolean drain, boolean echo) {
+            this.credit = credit;
+            this.deliveryCount = deliveryCount;
+            this.drain = drain;
+            this.echo = echo;
+        }
+
+        @Override
+        public int getCredit() {
+            return credit;
+        }
+
+        @Override
+        public int getDeliveryCount() {
+            return deliveryCount;
+        }
+
+        @Override
+        public boolean isDrain() {
+            return drain;
+        }
+
+        @Override
+        public boolean isEcho() {
+            return echo;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonOutgoingDelivery.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonOutgoingDelivery.java
new file mode 100644
index 0000000..9e78fee
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonOutgoingDelivery.java
@@ -0,0 +1,316 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+/**
+ * Proton outgoing delivery implementation
+ */
+public class ProtonOutgoingDelivery implements OutgoingDelivery {
+
+    private static final long DELIVERY_INACTIVE = -1;
+    private static final long DELIVERY_ABORTED = -2;
+
+    private final ProtonSender link;
+
+    private long deliveryId = DELIVERY_INACTIVE;
+
+    private DeliveryTag deliveryTag;
+
+    private boolean complete;
+    private int messageFormat;
+    private boolean aborted;
+    private int transferCount;
+
+    private DeliveryState localState;
+    private boolean locallySettled;
+
+    private DeliveryState remoteState;
+    private boolean remotelySettled;
+
+    private ProtonAttachments attachments;
+    private Object linkedResource;
+
+    private EventHandler<OutgoingDelivery> deliveryUpdatedEventHandler = null;
+
+    public ProtonOutgoingDelivery(ProtonSender link) {
+        this.link = link;
+    }
+
+    @Override
+    public ProtonSender getLink() {
+        return link;
+    }
+
+    @Override
+    public ProtonAttachments getAttachments() {
+        return attachments == null ? attachments = new ProtonAttachments() : attachments;
+    }
+
+    @Override
+    public ProtonOutgoingDelivery setLinkedResource(Object resource) {
+        this.linkedResource = resource;
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T getLinkedResource() {
+        return (T) linkedResource;
+    }
+
+    @Override
+    public <T> T getLinkedResource(Class<T> typeClass) {
+        return typeClass.cast(linkedResource);
+    }
+
+    @Override
+    public DeliveryTag getTag() {
+        return deliveryTag;
+    }
+
+    @Override
+    public OutgoingDelivery setTag(byte[] deliveryTag) {
+        if (transferCount > 0) {
+            throw new IllegalStateException("Cannot change delivery tag once Delivery has sent Transfer frames");
+        }
+
+        if (this.deliveryTag != null) {
+            this.deliveryTag.release();
+            this.deliveryTag = null;
+        }
+
+        this.deliveryTag = new DeliveryTag.ProtonDeliveryTag(deliveryTag);
+
+        return this;
+    }
+
+    @Override
+    public OutgoingDelivery setTag(DeliveryTag deliveryTag) {
+        this.deliveryTag = deliveryTag;
+        return this;
+    }
+
+    @Override
+    public DeliveryState getState() {
+        return localState;
+    }
+
+    @Override
+    public DeliveryState getRemoteState() {
+        return remoteState;
+    }
+
+    @Override
+    public int getMessageFormat() {
+        return messageFormat;
+    }
+
+    @Override
+    public OutgoingDelivery setMessageFormat(int messageFormat) {
+        if (transferCount > 0 && this.messageFormat != messageFormat) {
+            throw new IllegalStateException("Cannot change the message format once Delivery has sent Transfer frames");
+        }
+
+        this.messageFormat = messageFormat;
+        return this;
+    }
+
+    @Override
+    public boolean isPartial() {
+        return !complete && !aborted;
+    }
+
+    @Override
+    public boolean isAborted() {
+        return aborted;
+    }
+
+    @Override
+    public boolean isSettled() {
+        return locallySettled;
+    }
+
+    @Override
+    public boolean isRemotelySettled() {
+        return remotelySettled;
+    }
+
+    @Override
+    public OutgoingDelivery disposition(DeliveryState state) {
+        return disposition(state, false);
+    }
+
+    @Override
+    public OutgoingDelivery disposition(DeliveryState state, boolean settle) {
+        if (locallySettled) {
+            if ((localState != null && !localState.equals(state)) || localState != state) {
+                throw new IllegalStateException("Cannot update disposition on an already settled Delivery");
+            } else {
+                return this;
+            }
+        }
+
+        final DeliveryState oldState = localState;
+
+        this.locallySettled = settle;
+        this.localState = state;
+
+        // If no transfers initiated yet we just store the state and transmit in the first transfer
+        // and if no work actually requested we don't emit a useless frame.  After complete send we
+        // must send a disposition instead for this transfer until it is settled.
+        if (complete && (oldState != localState || settle)) {
+            try {
+                link.disposition(this);
+            } finally {
+                tryRetireDeliveryTag();
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public OutgoingDelivery settle() {
+        return disposition(localState, true);
+    }
+
+    @Override
+    public OutgoingDelivery writeBytes(ProtonBuffer buffer) {
+        checkCompleteOrAborted();
+        try {
+            link.send(this, buffer, true);
+        } finally {
+            tryRetireDeliveryTag();
+        }
+        return this;
+    }
+
+    @Override
+    public OutgoingDelivery streamBytes(ProtonBuffer buffer) {
+        return streamBytes(buffer, false);
+    }
+
+    @Override
+    public OutgoingDelivery streamBytes(ProtonBuffer buffer, boolean complete) {
+        checkCompleteOrAborted();
+        try {
+            link.send(this, buffer, complete);
+        } finally {
+            tryRetireDeliveryTag();
+        }
+        return this;
+    }
+
+    @Override
+    public OutgoingDelivery abort() {
+        checkComplete();
+
+        // Cannot abort when nothing has been sent so far.
+        if (deliveryId != DELIVERY_ABORTED) {
+            locallySettled = true;
+            aborted = true;
+            try {
+                link.abort(this);
+            } finally {
+                tryRetireDeliveryTag();
+                deliveryId = DELIVERY_ABORTED;
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonOutgoingDelivery deliveryStateUpdatedHandler(EventHandler<OutgoingDelivery> handler) {
+        this.deliveryUpdatedEventHandler = handler;
+        return this;
+    }
+
+    EventHandler<OutgoingDelivery> deliveryStateUpdatedHandler() {
+        return deliveryUpdatedEventHandler;
+    }
+
+    //----- Internal methods meant only for use by Proton resources
+
+    private void tryRetireDeliveryTag() {
+        if (deliveryTag != null && isSettled()) {
+            deliveryTag.release();
+        }
+    }
+
+    long getDeliveryId() {
+        return deliveryId;
+    }
+
+    void setDeliveryId(long deliveryId) {
+        this.deliveryId = deliveryId;
+    }
+
+    @Override
+    public int getTransferCount() {
+        return transferCount;
+    }
+
+    void afterTransferWritten() {
+        transferCount++;
+    }
+
+    ProtonOutgoingDelivery remotelySettled() {
+        this.remotelySettled = true;
+        return this;
+    }
+
+    ProtonOutgoingDelivery remoteState(DeliveryState remoteState) {
+        this.remoteState = remoteState;
+        return this;
+    }
+
+    ProtonOutgoingDelivery locallySettled() {
+        this.locallySettled = true;
+        return this;
+    }
+
+    ProtonOutgoingDelivery localState(DeliveryState localState) {
+        this.localState = localState;
+        return this;
+    }
+
+    ProtonOutgoingDelivery markComplete() {
+        this.complete = true;
+        return this;
+    }
+
+    //----- Private helper methods
+
+    private void checkComplete() {
+        if (complete) {
+            throw new IllegalArgumentException("Cannot write to a delivery already marked as complete.");
+        }
+    }
+
+    private void checkCompleteOrAborted() {
+        if (complete || aborted) {
+            throw new IllegalArgumentException("Cannot write to a delivery already marked as complete or has been aborted.");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonPerformativeHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonPerformativeHandler.java
new file mode 100644
index 0000000..330d815
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonPerformativeHandler.java
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader.HeaderHandler;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.apache.qpid.protonj2.types.transport.Performative.PerformativeHandler;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Transport Handler that forwards the incoming Performatives to the associated Connection
+ * as well as any error encountered during the Transport processing.
+ */
+public class ProtonPerformativeHandler implements EngineHandler, HeaderHandler<EngineHandlerContext>, PerformativeHandler<EngineHandlerContext> {
+
+    private ProtonEngine engine;
+    private ProtonConnection connection;
+    private ProtonEngineConfiguration configuration;
+
+    //----- Handle transport events
+
+    @Override
+    public void handlerAdded(EngineHandlerContext context) {
+        engine = (ProtonEngine) context.engine();
+        connection = engine.connection();
+        configuration = engine.configuration();
+
+        ((ProtonEngineHandlerContext) context).interestMask(ProtonEngineHandlerContext.HANDLER_READS);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+        header.invoke(this, context);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope envelope) {
+        try {
+            envelope.invoke(this, context);
+        } finally {
+            envelope.release();
+        }
+    }
+
+    @Override
+    public void engineFailed(EngineHandlerContext context, EngineFailedException failure) {
+        // In case external source injects failure we grab it and propagate after the
+        // appropriate changes to our engine state.
+        if (!engine.isFailed()) {
+            engine.engineFailed(failure.getCause());
+        }
+    }
+
+    //----- Deal with the incoming AMQP performatives
+
+    // Here we can spy on incoming performatives and update engine state relative to
+    // those prior to sending along notifications to other handlers or to the connection.
+    //
+    // We currently can't spy on outbound performatives but we could in future by splitting these
+    // into inner classes for inbound and outbound and handle the write to invoke the outbound
+    // handlers.
+
+    @Override
+    public void handleAMQPHeader(AMQPHeader header, EngineHandlerContext context) {
+        // Recompute max frame size now based on engine max frame size in case sasl was enabled.
+        configuration.recomputeEffectiveFrameSizeLimits();
+
+        // Let the Connection know we have a header so it can emit any pending work.
+        header.invoke(connection, engine);
+    }
+
+    @Override
+    public void handleSASLHeader(AMQPHeader header, EngineHandlerContext context) {
+        // Respond with Raw AMQP Header and then fail the engine.
+        context.fireWrite(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+
+        throw new ProtocolViolationException("Received SASL Header but no SASL support configured");
+    }
+
+    @Override
+    public void handleOpen(Open open, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        if (channel != 0) {
+            throw new ProtocolViolationException("Open not sent on channel zero");
+        }
+
+        connection.handleOpen(open, payload, channel, engine);
+
+        // Recompute max frame size now based on what remote told us.
+        configuration.recomputeEffectiveFrameSizeLimits();
+    }
+
+    @Override
+    public void handleBegin(Begin begin, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleBegin(begin, payload, channel, engine);
+    }
+
+    @Override
+    public void handleAttach(Attach attach, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleAttach(attach, payload, channel, engine);
+    }
+
+    @Override
+    public void handleFlow(Flow flow, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleFlow(flow, payload, channel, engine);
+    }
+
+    @Override
+    public void handleTransfer(Transfer transfer, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleTransfer(transfer, payload, channel, engine);
+    }
+
+    @Override
+    public void handleDisposition(Disposition disposition, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleDisposition(disposition, payload, channel, engine);
+    }
+
+    @Override
+    public void handleDetach(Detach detach, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleDetach(detach, payload, channel, engine);
+    }
+
+    @Override
+    public void handleEnd(End end, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleEnd(end, payload, channel, engine);
+    }
+
+    @Override
+    public void handleClose(Close close, ProtonBuffer payload, int channel, EngineHandlerContext context) {
+        connection.handleClose(close, payload, channel, engine);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonPooledTagGenerator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonPooledTagGenerator.java
new file mode 100644
index 0000000..6b46c76
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonPooledTagGenerator.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.Queue;
+
+import org.apache.qpid.protonj2.engine.util.RingQueue;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+
+/**
+ * Built in Transfer {@link DeliveryTag} generator that uses a fixed size tag
+ * pool to reduce GC overhead by reusing tags that have been released from settled
+ * messages.  When not using cached tags the generator creates new tags using a
+ * running tag counter of type {@link Long} that assumes that when it wraps the user
+ * has already release all tags within the lower range of the tag counter.
+ */
+public class ProtonPooledTagGenerator extends ProtonSequentialTagGenerator {
+
+    public static final int DEFAULT_MAX_NUM_POOLED_TAGS = 512;
+
+    private final int tagPoolSize;
+    private final Queue<ProtonPooledDeliveryTag> tagPool;
+
+    public ProtonPooledTagGenerator() {
+        this(DEFAULT_MAX_NUM_POOLED_TAGS);
+    }
+
+    public ProtonPooledTagGenerator(int poolSize) {
+        if (poolSize == 0) {
+            throw new IllegalArgumentException("Cannot create a tag pool with zero pool size");
+        }
+
+        if (poolSize < 0) {
+            throw new IllegalArgumentException("Cannot create a tag pool with negative pool size");
+        }
+
+        tagPoolSize = poolSize;
+        tagPool = new RingQueue<>(tagPoolSize);
+    }
+
+    @Override
+    public DeliveryTag nextTag() {
+        ProtonPooledDeliveryTag nextTag = tagPool.poll();
+        if (nextTag != null) {
+            return nextTag.checkOut();
+        } else {
+            return createTag();
+        }
+    }
+
+    private DeliveryTag createTag() {
+        DeliveryTag nextTag = null;
+
+        if (nextTagId >= 0 && nextTagId < tagPoolSize) {
+            // Pooled tag that will return to pool on next release.
+            nextTag = new ProtonPooledDeliveryTag((byte) nextTagId++).checkOut();
+        } else {
+            // Non-pooled tag that will not return to the pool on next release.
+            nextTag = super.nextTag();
+            if (nextTagId == 0) {
+                nextTagId = tagPoolSize;
+            }
+        }
+
+        return nextTag;
+    }
+
+    /*
+     * Test entry point to validate tag pool and tag counter overflow.
+     */
+    @Override
+    void setNextTagId(long nextIdValue) {
+        this.nextTagId = nextIdValue;
+    }
+
+    //----- Specialized DeliveryTag and releases itself back to the pool
+
+    private class ProtonPooledDeliveryTag extends ProtonNumericDeliveryTag {
+
+        private boolean checkedOut;
+
+        public ProtonPooledDeliveryTag(long tagValue) {
+            super(tagValue);
+        }
+
+        public ProtonPooledDeliveryTag checkOut() {
+            this.checkedOut = true;
+            return this;
+        }
+
+        @Override
+        public void release() {
+            if (checkedOut) {
+                tagPool.offer(this);
+                checkedOut = false;
+            }
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonReceiver.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonReceiver.java
new file mode 100644
index 0000000..4cb0d7a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonReceiver.java
@@ -0,0 +1,461 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.LinkCreditState;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.util.DeliveryIdTracker;
+import org.apache.qpid.protonj2.engine.util.LinkedSplayMap;
+import org.apache.qpid.protonj2.engine.util.SplayMap;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Proton Receiver link implementation.
+ */
+public class ProtonReceiver extends ProtonLink<Receiver> implements Receiver {
+
+    private EventHandler<IncomingDelivery> deliveryReadEventHandler = null;
+    private EventHandler<IncomingDelivery> deliveryAdbortedEventHandler = null;
+    private EventHandler<IncomingDelivery> deliveryUpdatedEventHandler = null;
+    private EventHandler<Receiver> linkCreditUpdatedHandler = null;
+
+    private final ProtonSessionIncomingWindow sessionWindow;
+    private final DeliveryIdTracker currentDeliveryId = new DeliveryIdTracker();
+    private final SplayMap<ProtonIncomingDelivery> unsettled = new LinkedSplayMap<>();
+
+    private DeliveryState defaultDeliveryState;
+    private LinkCreditState drainStateSnapshot;
+
+    /**
+     * Create a new {@link Receiver} instance with the given {@link Session} parent.
+     *
+     *  @param session
+     *      The Session that is linked to this receiver instance.
+     *  @param name
+     *      The name assigned to this {@link Receiver} link.
+     */
+    public ProtonReceiver(ProtonSession session, String name) {
+        super(session, name, new ProtonLinkCreditState());
+
+        this.sessionWindow = session.getIncomingWindow();
+    }
+
+    @Override
+    public ProtonReceiver setDefaultDeliveryState(DeliveryState state) {
+        this.defaultDeliveryState = state;
+        return this;
+    }
+
+    @Override
+    public DeliveryState getDefaultDeliveryState() {
+        return defaultDeliveryState;
+    }
+
+    @Override
+    public Role getRole() {
+        return Role.RECEIVER;
+    }
+
+    @Override
+    protected ProtonReceiver self() {
+        return this;
+    }
+
+    @Override
+    public int getCredit() {
+        return getCreditState().getCredit();
+    }
+
+    @Override
+    public ProtonReceiver addCredit(int credit) {
+        checkLinkOperable("Cannot add credit");
+
+        if (credit < 0) {
+            throw new IllegalArgumentException("additional credits cannot be less than zero");
+        }
+
+        if (credit > 0) {
+            getCreditState().incrementCredit(credit);
+            if (isLocallyOpen() && wasLocalAttachSent()) {
+                sessionWindow.writeFlow(this);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public boolean drain() {
+        checkLinkOperable("Cannot drain Receiver");
+
+        if (drainStateSnapshot != null) {
+            throw new IllegalStateException("Drain attempt already outstanding");
+        }
+
+        if (getCredit() > 0) {
+            drainStateSnapshot = getCreditState().snapshot();
+
+            if (isLocallyOpen() && wasLocalAttachSent()) {
+                sessionWindow.writeFlow(this);
+            }
+        }
+
+        return isDraining();
+    }
+
+    @Override
+    public boolean drain(int credits) {
+        checkLinkOperable("Cannot drain Receiver");
+
+        if (drainStateSnapshot != null) {
+            throw new IllegalStateException("Drain attempt already outstanding");
+        }
+
+        final int currentCredit = getCredit();
+
+        if (credits < 0) {
+            throw new IllegalArgumentException("Cannot drain negative link credit");
+        }
+
+        if (credits < currentCredit) {
+            throw new IllegalArgumentException("Cannot drain partial link credit");
+        }
+
+        getCreditState().incrementCredit(credits - currentCredit);
+
+        if (getCredit() > 0) {
+            drainStateSnapshot = getCreditState().snapshot();
+
+            if (isLocallyOpen() && wasLocalAttachSent()) {
+                sessionWindow.writeFlow(this);
+            }
+        }
+
+        return isDraining();
+    }
+
+    @Override
+    public boolean isDraining() {
+        return drainStateSnapshot != null;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Receiver disposition(Predicate<IncomingDelivery> filter, DeliveryState disposition, boolean settle) {
+        checkLinkOperable("Cannot apply disposition");
+        Objects.requireNonNull(filter, "Supplied filter cannot be null");
+
+        List<UnsignedInteger> toRemove = settle ? new ArrayList<>() : Collections.EMPTY_LIST;
+
+        unsettled.forEach((deliveryId, delivery) -> {
+            if (filter.test(delivery)) {
+                if (disposition != null) {
+                    delivery.localState(disposition);
+                }
+                if (settle) {
+                    delivery.locallySettled();
+                    toRemove.add(deliveryId);
+                }
+                sessionWindow.processDisposition(this, delivery);
+            }
+        });
+
+        if (!toRemove.isEmpty()) {
+            toRemove.forEach(deliveryId -> unsettled.remove(deliveryId));
+        }
+
+        return this;
+    }
+
+    @Override
+    public Receiver settle(Predicate<IncomingDelivery> filter) {
+        return disposition(filter, null, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<IncomingDelivery> unsettled() {
+        if (unsettled.isEmpty()) {
+            return Collections.EMPTY_LIST;
+        } else {
+            return Collections.unmodifiableCollection(new ArrayList<>(unsettled.values()));
+        }
+    }
+
+    @Override
+    public boolean hasUnsettled() {
+        return !unsettled.isEmpty();
+    }
+
+    //----- Delivery related access points
+
+    void disposition(ProtonIncomingDelivery delivery) {
+        if (!delivery.isRemotelySettled()) {
+            checkLinkOperable("Cannot set a disposition for delivery");
+        }
+
+        try {
+            sessionWindow.processDisposition(this, delivery);
+        } finally {
+            if (delivery.isSettled()) {
+                unsettled.remove((int) delivery.getDeliveryId());
+                if (delivery.getTag() != null) {
+                    delivery.getTag().release();
+                }
+            }
+        }
+    }
+
+    void deliveryRead(ProtonIncomingDelivery delivery, int bytesRead) {
+        if (areDeliveriesStillActive()) {
+            sessionWindow.deliveryRead(delivery, bytesRead);
+        }
+    }
+
+    //----- Receiver event handlers
+
+    @Override
+    public Receiver deliveryReadHandler(EventHandler<IncomingDelivery> handler) {
+        this.deliveryReadEventHandler = handler;
+        return this;
+    }
+
+    Receiver signalDeliveryRead(ProtonIncomingDelivery delivery) {
+        if (delivery.deliveryReadHandler() != null) {
+            delivery.deliveryReadHandler().handle(delivery);
+        } else if (deliveryReadEventHandler != null) {
+            deliveryReadEventHandler.handle(delivery);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Receiver deliveryAbortedHandler(EventHandler<IncomingDelivery> handler) {
+        this.deliveryAdbortedEventHandler = handler;
+        return this;
+    }
+
+    Receiver signalDeliveryAborted(ProtonIncomingDelivery delivery) {
+        if (delivery.deliveryAbortedHandler() != null) {
+            delivery.deliveryAbortedHandler().handle(delivery);
+        } else if (delivery.deliveryReadHandler() != null) {
+            delivery.deliveryReadHandler().handle(delivery);
+        } else if (deliveryAdbortedEventHandler != null) {
+            deliveryAdbortedEventHandler.handle(delivery);
+        } else if (deliveryReadEventHandler != null) {
+            deliveryReadEventHandler.handle(delivery);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Receiver deliveryStateUpdatedHandler(EventHandler<IncomingDelivery> handler) {
+        this.deliveryUpdatedEventHandler = handler;
+        return this;
+    }
+
+    Receiver signalDeliveryStateUpdated(ProtonIncomingDelivery delivery) {
+        if (delivery.deliveryStateUpdatedHandler() != null) {
+            delivery.deliveryStateUpdatedHandler().handle(delivery);
+        } else if (deliveryUpdatedEventHandler != null) {
+            deliveryUpdatedEventHandler.handle(delivery);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Receiver creditStateUpdateHandler(EventHandler<Receiver> handler) {
+        this.linkCreditUpdatedHandler = handler;
+        return this;
+    }
+
+    Receiver signalLinkCreditStateUpdated() {
+        if (linkCreditUpdatedHandler != null) {
+            linkCreditUpdatedHandler.handle(this);
+        }
+
+        return this;
+    }
+
+    //----- Handle incoming frames from the remote sender
+
+    @Override
+    protected final ProtonReceiver handleRemoteAttach(Attach attach) {
+        if (!attach.hasInitialDeliveryCount()) {
+            throw new ProtocolViolationException("Sending peer attach had no initial delivery count");
+        }
+
+        getCreditState().initialiseDeliveryCount((int) attach.getInitialDeliveryCount());
+
+        return this;
+    }
+
+    @Override
+    protected final ProtonReceiver handleRemoteDetach(Detach detach) {
+        return this;
+    }
+
+    @Override
+    protected final ProtonReceiver handleRemoteFlow(Flow flow) {
+        ProtonLinkCreditState creditState = getCreditState();
+        creditState.remoteFlow(flow);
+
+        if (flow.getDrain()) {
+            creditState.updateDeliveryCount((int) flow.getDeliveryCount());
+            creditState.updateCredit((int) flow.getLinkCredit());
+            if (creditState.getCredit() != 0) {
+                throw new IllegalArgumentException("Receiver read flow with drain set but credit was not zero");
+            } else {
+                drainStateSnapshot = null;
+            }
+        }
+
+        signalLinkCreditStateUpdated();
+
+        return this;
+    }
+
+    @Override
+    protected final ProtonReceiver handleRemoteDisposition(Disposition disposition, ProtonIncomingDelivery delivery) {
+        boolean updated = false;
+
+        if (disposition.getState() != null && !disposition.getState().equals(delivery.getRemoteState())) {
+            updated = true;
+            delivery.remoteState(disposition.getState());
+        }
+
+        if (disposition.getSettled() && !delivery.isRemotelySettled()) {
+            updated = true;
+            delivery.remotelySettled();
+        }
+
+        if (updated) {
+            signalDeliveryStateUpdated(delivery);
+        }
+
+        return this;
+    }
+
+    @Override
+    protected final ProtonReceiver handleRemoteDisposition(Disposition disposition, ProtonOutgoingDelivery delivery) {
+        throw new IllegalStateException("Receiver link should never handle dispsotiions for outgoing deliveries");
+    }
+
+    @Override
+    protected final ProtonIncomingDelivery handleRemoteTransfer(Transfer transfer, ProtonBuffer payload) {
+        final ProtonIncomingDelivery delivery;
+
+        if (!currentDeliveryId.isEmpty() && (!transfer.hasDeliveryId() || currentDeliveryId.equals((int) transfer.getDeliveryId()))) {
+            delivery = unsettled.get(currentDeliveryId.intValue());
+        } else {
+            verifyNewDeliveryIdSequence(transfer, currentDeliveryId);
+
+            delivery = new ProtonIncomingDelivery(this, transfer.getDeliveryId(), transfer.getDeliveryTag());
+            delivery.setMessageFormat((int) transfer.getMessageFormat());
+
+            unsettled.put((int) transfer.getDeliveryId(), delivery);
+            currentDeliveryId.set((int) transfer.getDeliveryId());
+        }
+
+        if (transfer.hasState()) {
+            delivery.remoteState(transfer.getState());
+        }
+
+        if (transfer.getSettled() || transfer.getAborted()) {
+            delivery.remotelySettled();
+        }
+
+        if (payload != null) {
+            delivery.appendTransferPayload(payload);
+        }
+
+        final boolean done = transfer.getAborted() || !transfer.getMore();
+        if (done) {
+            getCreditState().decrementCredit();
+            getCreditState().incrementDeliveryCount();
+            currentDeliveryId.reset();
+
+            if (transfer.getAborted()) {
+                delivery.aborted();
+            } else {
+                delivery.completed();
+            }
+        }
+
+        if (transfer.getAborted()) {
+            signalDeliveryAborted(delivery);
+        } else {
+            signalDeliveryRead(delivery);
+        }
+
+        if (isDraining() && getCredit() == 0) {
+            drainStateSnapshot = null;
+            signalLinkCreditStateUpdated();
+        }
+
+        return delivery;
+    }
+
+    @Override
+    protected ProtonReceiver decorateOutgoingFlow(Flow flow) {
+        flow.setLinkCredit(getCredit());
+        flow.setHandle(getHandle());
+        if (getCreditState().isDeliveryCountInitalised()) {
+            flow.setDeliveryCount(getCreditState().getDeliveryCount());
+        }
+        flow.setDrain(isDraining());
+
+        return this;
+    }
+
+    private void verifyNewDeliveryIdSequence(Transfer transfer, DeliveryIdTracker currentDeliveryId) {
+        if (!transfer.hasDeliveryId()) {
+            getEngine().engineFailed(
+                new ProtocolViolationException("No delivery-id specified on first Transfer of new delivery"));
+        }
+
+        sessionWindow.validateNextDeliveryId(transfer.getDeliveryId());
+
+        if (!currentDeliveryId.isEmpty()) {
+            getEngine().engineFailed(
+                new ProtocolViolationException("Illegal multiplex of deliveries on same link with delivery-id " +
+                                               currentDeliveryId + " and " + transfer.getDeliveryId()));
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSender.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSender.java
new file mode 100644
index 0000000..e40c7bf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSender.java
@@ -0,0 +1,426 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.util.DeliveryIdTracker;
+import org.apache.qpid.protonj2.engine.util.LinkedSplayMap;
+import org.apache.qpid.protonj2.engine.util.SplayMap;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Proton Sender link implementation.
+ */
+public class ProtonSender extends ProtonLink<Sender> implements Sender {
+
+    private final ProtonSessionOutgoingWindow sessionWindow;
+    private final DeliveryIdTracker currentDeliveryId = new DeliveryIdTracker();
+    private final SplayMap<ProtonOutgoingDelivery> unsettled = new LinkedSplayMap<>();
+
+    private EventHandler<OutgoingDelivery> deliveryUpdatedEventHandler = null;
+    private EventHandler<Sender> linkCreditUpdatedHandler = null;
+
+    private boolean sendable;
+    private DeliveryTagGenerator autoTagGenerator;
+    private OutgoingDelivery current;
+
+    /**
+     * Create a new {@link Sender} instance with the given {@link Session} parent.
+     *
+     *  @param session
+     *      The Session that is linked to this sender instance.
+     *  @param name
+     *      The name assigned to this {@link Sender} link.
+     */
+    public ProtonSender(ProtonSession session, String name) {
+        super(session, name, new ProtonLinkCreditState(0));
+
+        this.sessionWindow = session.getOutgoingWindow();
+    }
+
+    @Override
+    public Role getRole() {
+        return Role.SENDER;
+    }
+
+    @Override
+    protected ProtonSender self() {
+        return this;
+    }
+
+    @Override
+    public int getCredit() {
+        return getCreditState().getCredit();
+    }
+
+    @Override
+    public boolean isSendable() {
+        return sendable && sessionWindow.isSendable();
+    }
+
+    @Override
+    public boolean isDraining() {
+        return getCreditState().isDrain();
+    }
+
+    @Override
+    public Sender drained() {
+        checkLinkOperable("Cannot report link drained.");
+
+        final ProtonLinkCreditState state = getCreditState();
+
+        if (state.isDrain() && state.hasCredit()) {
+            int drained = state.getCredit();
+
+            state.clearCredit();
+            state.incrementDeliveryCount(drained);
+
+            session.writeFlow(this);
+
+            state.clearDrain();
+        }
+
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Sender disposition(Predicate<OutgoingDelivery> filter, DeliveryState state, boolean settle) {
+        checkLinkOperable("Cannot apply disposition");
+        Objects.requireNonNull(filter, "Supplied filter cannot be null");
+
+        List<UnsignedInteger> toRemove = settle ? new ArrayList<>() : Collections.EMPTY_LIST;
+
+        unsettled.forEach((deliveryId, delivery) -> {
+            if (filter.test(delivery)) {
+                if (state != null) {
+                    delivery.localState(state);
+                }
+                if (settle) {
+                    delivery.locallySettled();
+                    toRemove.add(deliveryId);
+                }
+                sessionWindow.processDisposition(this, delivery);
+            }
+        });
+
+        if (!toRemove.isEmpty()) {
+            toRemove.forEach(deliveryId -> unsettled.remove(deliveryId));
+        }
+
+        return this;
+    }
+
+    @Override
+    public Sender settle(Predicate<OutgoingDelivery> filter) {
+        disposition(filter, null, true);
+        return this;
+    }
+
+    @Override
+    public OutgoingDelivery current() {
+        return current;
+    }
+
+    @Override
+    public OutgoingDelivery next() {
+        checkLinkOperable("Cannot update next delivery");
+
+        if (current != null) {
+            throw new IllegalStateException("Current delivery is not complete and cannot be advanced.");
+        } else {
+            current = new ProtonOutgoingDelivery(this);
+            if (autoTagGenerator != null) {
+                current.setTag(autoTagGenerator.nextTag());
+            }
+        }
+
+        return current;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<OutgoingDelivery> unsettled() {
+        if (unsettled.isEmpty()) {
+            return Collections.EMPTY_LIST;
+        } else {
+            return Collections.unmodifiableCollection(new ArrayList<>(unsettled.values()));
+        }
+    }
+
+    @Override
+    public boolean hasUnsettled() {
+        return !unsettled.isEmpty();
+    }
+
+    @Override
+    public Sender setDeliveryTagGenerator(DeliveryTagGenerator generator) {
+        this.autoTagGenerator = generator;
+        return this;
+    }
+
+    @Override
+    public DeliveryTagGenerator getDeliveryTagGenerator() {
+        return autoTagGenerator;
+    }
+
+    //----- Handle remote events for this Sender
+
+    @Override
+    protected final ProtonSender handleRemoteAttach(Attach attach) {
+        return this;
+    }
+
+    @Override
+    protected final ProtonSender handleRemoteDetach(Detach detach) {
+        return this;
+    }
+
+    @Override
+    protected final ProtonSender handleRemoteDisposition(Disposition disposition, ProtonIncomingDelivery delivery) {
+        throw new IllegalStateException("Sender link should never handle dispsotiions for incoming deliveries");
+    }
+
+    @Override
+    protected final ProtonSender handleRemoteDisposition(Disposition disposition, ProtonOutgoingDelivery delivery) {
+        boolean updated = false;
+
+        if (disposition.getState() != null && !disposition.getState().equals(delivery.getRemoteState())) {
+            updated = true;
+            delivery.remoteState(disposition.getState());
+        }
+
+        if (disposition.getSettled() && !delivery.isRemotelySettled()) {
+            updated = true;
+            delivery.remotelySettled();
+        }
+
+        if (updated) {
+            delivery.getLink().signalDeliveryStateUpdated(delivery);
+        }
+
+        return this;
+    }
+
+    @Override
+    protected final ProtonIncomingDelivery handleRemoteTransfer(Transfer transfer, ProtonBuffer payload) {
+        throw new IllegalArgumentException("Sender end cannot process incoming transfers");
+    }
+
+    @Override
+    protected final ProtonSender handleRemoteFlow(Flow flow) {
+        ProtonLinkCreditState creditState = getCreditState();
+
+        creditState.remoteFlow(flow);
+
+        int existingDeliveryCount = creditState.getDeliveryCount();
+        // int casts are expected, credit is a uint and delivery-count is really a uint sequence which wraps, so we
+        // just use the truncation and overflows.  Receivers flow might not have any delivery-count, as sender initializes
+        // on attach! We initialize to 0 so we can just ignore that.
+        int remoteDeliveryCount = (int) flow.getDeliveryCount();
+        int newDeliveryCountLimit = remoteDeliveryCount + (int) flow.getLinkCredit();
+
+        long effectiveCredit = 0xFFFFFFFFL & newDeliveryCountLimit - existingDeliveryCount;
+        if (effectiveCredit > 0) {
+            creditState.updateCredit((int) effectiveCredit);
+        } else {
+            creditState.updateCredit(0);
+        }
+
+        if (isLocallyOpen()) {
+            sendable = getCredit() > 0 && sessionWindow.isSendable();
+
+            signalLinkCreditStateUpdated();
+        }
+
+        return this;
+    }
+
+    ProtonSender handleSessionCreditStateUpdate(ProtonSessionOutgoingWindow protonSessionOutgoingWindow) {
+        final boolean previousSendable = sendable;
+
+        sendable = getCredit() > 0 && sessionWindow.isSendable();
+
+        if (previousSendable != sendable) {
+            signalLinkCreditStateUpdated();
+        }
+
+        return this;
+    }
+
+    @Override
+    protected final ProtonSender decorateOutgoingFlow(Flow flow) {
+        flow.setLinkCredit(getCredit());
+        flow.setHandle(getHandle());
+        flow.setDeliveryCount(getCreditState().getDeliveryCount());
+        flow.setDrain(isDraining());
+
+        return this;
+    }
+
+    //----- Delivery output related access points
+
+    void send(ProtonOutgoingDelivery delivery, ProtonBuffer buffer, boolean complete) {
+        checkLinkOperable("Cannot send when link has become inoperable");
+
+        if (isSendable()) {
+            if (currentDeliveryId.isEmpty()) {
+                currentDeliveryId.set(sessionWindow.getAndIncrementNextDeliveryId());
+
+                delivery.setDeliveryId(currentDeliveryId.longValue());
+            }
+
+            if (!delivery.isSettled()) {
+                unsettled.put((int) delivery.getDeliveryId(), delivery);
+            }
+
+            try {
+                sendable = sessionWindow.processSend(this, delivery, buffer, complete) && getCredit() > 0;
+            } finally {
+                if (complete && (buffer == null || !buffer.isReadable())) {
+                    delivery.markComplete();
+                    currentDeliveryId.reset();
+                    current = null;
+                    getCreditState().incrementDeliveryCount();
+                    getCreditState().decrementCredit();
+
+                    if (getCredit() == 0) {
+                        sendable = false;
+                        getCreditState().clearDrain();
+                    }
+                }
+            }
+        }
+    }
+
+    void disposition(ProtonOutgoingDelivery delivery) {
+        if (!delivery.isRemotelySettled()) {
+            checkLinkOperable("Cannot set a disposition");
+        }
+
+        try {
+            sessionWindow.processDisposition(this, delivery);
+        } finally {
+            if (delivery.isSettled()) {
+                unsettled.remove((int) delivery.getDeliveryId());
+            }
+        }
+    }
+
+    void abort(ProtonOutgoingDelivery delivery) {
+        checkLinkOperable("Cannot abort Transfer");
+
+        try {
+            if (delivery.getTransferCount() > 0) {
+                sessionWindow.processAbort(this, delivery);
+            }
+        } finally {
+            unsettled.remove((int) delivery.getDeliveryId());
+            currentDeliveryId.reset();
+            current = null;
+        }
+    }
+
+    //----- Sender event handlers
+
+    @Override
+    public Sender creditStateUpdateHandler(EventHandler<Sender> handler) {
+        this.linkCreditUpdatedHandler = handler;
+        return this;
+    }
+
+    Sender signalLinkCreditStateUpdated() {
+        if (linkCreditUpdatedHandler != null) {
+            linkCreditUpdatedHandler.handle(this);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Sender deliveryStateUpdatedHandler(EventHandler<OutgoingDelivery> handler) {
+        this.deliveryUpdatedEventHandler = handler;
+        return this;
+    }
+
+    Sender signalDeliveryStateUpdated(ProtonOutgoingDelivery delivery) {
+        if (delivery.deliveryStateUpdatedHandler() != null) {
+            delivery.deliveryStateUpdatedHandler().handle(delivery);
+        } else if (deliveryUpdatedEventHandler != null) {
+            deliveryUpdatedEventHandler.handle(delivery);
+        }
+
+        return this;
+    }
+
+    //----- Internal routing and state management
+
+    @Override
+    protected void transitionedToLocallyOpened() {
+        localAttach.setInitialDeliveryCount(currentDeliveryId.longValue());
+        sendable = getCredit() > 0 && sessionWindow.isSendable();
+    }
+
+    @Override
+    protected void transitionedToLocallyDetached() {
+        sendable = false;
+    }
+
+    @Override
+    protected void transitionedToLocallyClosed() {
+        sendable = false;
+    }
+
+    @Override
+    protected void transitionToRemotelyDetached() {
+        sendable = false;
+    }
+
+    @Override
+    protected void transitionToRemotelyCosed() {
+        sendable = false;
+    }
+
+    @Override
+    protected void transitionToParentLocallyClosed() {
+        sendable = false;
+    }
+
+    @Override
+    protected void transitionToParentRemotelyClosed() {
+        sendable = false;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSequentialTagGenerator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSequentialTagGenerator.java
new file mode 100644
index 0000000..e38833b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSequentialTagGenerator.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonByteUtils;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+
+/**
+ * A Built in proton {@link DeliveryTagGenerator} that creates new tags using a sequential
+ * numeric value which is encoded using the most compact representation of the numeric value.
+ */
+public class ProtonSequentialTagGenerator extends ProtonDeliveryTagGenerator {
+
+    protected long nextTagId = 0;
+
+    @Override
+    public DeliveryTag nextTag() {
+        return new ProtonNumericDeliveryTag(nextTagId++);
+    }
+
+    /*
+     * Test entry point to validate tag cache and tag counter overflow.
+     */
+    void setNextTagId(long nextIdValue) {
+        this.nextTagId = nextIdValue;
+    }
+
+    protected static class ProtonNumericDeliveryTag implements DeliveryTag {
+
+        protected final long tagValue;
+
+        public ProtonNumericDeliveryTag(long tagValue) {
+            this.tagValue = tagValue;
+        }
+
+        @Override
+        public int tagLength() {
+            if (tagValue < 0) {
+                return Long.BYTES;
+            } else if (tagValue <= 0x00000000000000FFl) {
+                return Byte.BYTES;
+            } else if (tagValue <= 0x000000000000FFFFl) {
+                return Short.BYTES;
+            } else if (tagValue <= 0x00000000FFFFFFFFl) {
+                return Integer.BYTES;
+            } else {
+                return Long.BYTES;
+            }        }
+
+        @Override
+        public byte[] tagBytes() {
+            if (tagValue < 0) {
+                return ProtonByteUtils.toByteArray(tagValue);
+            } else if (tagValue <= 0x00000000000000FFl) {
+                return ProtonByteUtils.toByteArray((byte) tagValue);
+            } else if (tagValue <= 0x000000000000FFFFl) {
+                return ProtonByteUtils.toByteArray((short) tagValue);
+            } else if (tagValue <= 0x00000000FFFFFFFFl) {
+                return ProtonByteUtils.toByteArray((int) tagValue);
+            } else {
+                return ProtonByteUtils.toByteArray(tagValue);
+            }
+        }
+
+        @Override
+        public ProtonBuffer tagBuffer() {
+            return ProtonByteBufferAllocator.DEFAULT.wrap(tagBytes());
+        }
+
+        @Override
+        public void release() {
+            // Nothing to do in this implementation
+        }
+
+        @Override
+        public DeliveryTag copy() {
+            return new ProtonNumericDeliveryTag(tagValue);
+        }
+
+        @Override
+        public int hashCode() {
+            return Long.hashCode(tagValue);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+
+            ProtonNumericDeliveryTag other = (ProtonNumericDeliveryTag) obj;
+            if (tagValue != other.tagValue) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "{" + tagValue + "}";
+        }
+
+        @Override
+        public void writeTo(ProtonBuffer buffer) {
+            if (tagValue < 0) {
+                buffer.writeLong(tagValue);
+            } else if (tagValue <= 0x00000000000000FFl) {
+                buffer.writeByte((int) tagValue);
+            } else if (tagValue <= 0x000000000000FFFFl) {
+                buffer.writeShort((short) tagValue);
+            } else if (tagValue <= 0x00000000FFFFFFFFl) {
+                buffer.writeInt((int) tagValue);
+            } else {
+                buffer.writeLong(tagValue);
+            }
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSession.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSession.java
new file mode 100644
index 0000000..7e79a4b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSession.java
@@ -0,0 +1,765 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.ConnectionState;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.Link;
+import org.apache.qpid.protonj2.engine.LinkState;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.SessionState;
+import org.apache.qpid.protonj2.engine.TransactionController;
+import org.apache.qpid.protonj2.engine.TransactionManager;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.util.SplayMap;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SessionError;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Proton API for Session type.
+ */
+public class ProtonSession extends ProtonEndpoint<Session> implements Session {
+
+    private final Begin localBegin = new Begin();
+    private Begin remoteBegin;
+
+    private int localChannel;
+
+    private final ProtonSessionOutgoingWindow outgoingWindow;
+    private final ProtonSessionIncomingWindow incomingWindow;
+
+    private final Map<String, ProtonSender> senderByNameMap = new HashMap<>();
+    private final Map<String, ProtonReceiver> receiverByNameMap = new HashMap<>();
+
+    private final SplayMap<ProtonLink<?>> localLinks = new SplayMap<>();
+    private final SplayMap<ProtonLink<?>> remoteLinks = new SplayMap<>();
+
+    private final Flow cachedFlow = new Flow();
+
+    private final ProtonConnection connection;
+
+    private SessionState localState = SessionState.IDLE;
+    private SessionState remoteState = SessionState.IDLE;
+
+    private boolean localBeginSent;
+    private boolean localEndSent;
+
+    // No default for these handlers, Connection will process these if not set here.
+    private EventHandler<Sender> remoteSenderOpenEventHandler;
+    private EventHandler<Receiver> remoteReceiverOpenEventHandler;
+    private EventHandler<TransactionManager> remoteTxnManagerOpenEventHandler;
+
+    public ProtonSession(ProtonConnection connection, int localChannel) {
+        super(connection.getEngine());
+
+        this.connection = connection;
+        this.localChannel = localChannel;
+
+        this.outgoingWindow = new ProtonSessionOutgoingWindow(this);
+        this.incomingWindow = new ProtonSessionIncomingWindow(this);
+    }
+
+    @Override
+    ProtonSession self() {
+        return this;
+    }
+
+    @Override
+    public ProtonConnection getConnection() {
+        return connection;
+    }
+
+    @Override
+    public ProtonConnection getParent() {
+        return connection;
+    }
+
+    public int getLocalChannel() {
+        return localChannel;
+    }
+
+    public int getRemoteChannel() {
+        return remoteBegin != null ? remoteBegin.getRemoteChannel() : -1;
+    }
+
+    @Override
+    public SessionState getState() {
+        return localState;
+    }
+
+    @Override
+    public SessionState getRemoteState() {
+        return remoteState;
+    }
+
+    @Override
+    public ProtonSession open() throws IllegalStateException, EngineStateException {
+        if (getState() == SessionState.IDLE) {
+            checkConnectionClosed();
+            getEngine().checkShutdownOrFailed("Cannot open a session when Engine is shutdown or failed.");
+
+            localState = SessionState.ACTIVE;
+            incomingWindow.configureOutbound(localBegin);
+            outgoingWindow.configureOutbound(localBegin);
+            try {
+                trySyncLocalStateWithRemote();
+            } finally {
+                fireLocalOpen();
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public ProtonSession close() throws EngineFailedException {
+        if (getState() == SessionState.ACTIVE) {
+            localState = SessionState.CLOSED;
+            try {
+                engine.checkFailed("Session close called but engine is in a failed state.");
+                trySyncLocalStateWithRemote();
+            } finally {
+                allLinks().forEach(link -> link.handleSessionLocallyClosed(this));
+                fireLocalClose();
+            }
+        }
+
+        return this;
+    }
+
+    //----- View and configure this end of the session endpoint
+
+    @Override
+    public boolean isLocallyOpen() {
+        return getState() == SessionState.ACTIVE;
+    }
+
+    @Override
+    public boolean isLocallyClosed() {
+        return getState() == SessionState.CLOSED;
+    }
+
+    @Override
+    public Session setIncomingCapacity(int incomingCapacity) {
+        incomingWindow.setIncomingCapaity(incomingCapacity);
+        return this;
+    }
+
+    @Override
+    public int getIncomingCapacity() {
+        return incomingWindow.getIncomingCapacity();
+    }
+
+    @Override
+    public int getRemainingIncomingCapacity() {
+        return incomingWindow.getRemainingIncomingCapacity();
+    }
+
+    @Override
+    public Session setOutgoingCapacity(int outgoingCapacity) {
+        outgoingWindow.setOutgoingCapacity(outgoingCapacity);
+        return this;
+    }
+
+    @Override
+    public int getOutgoingCapacity() {
+        return outgoingWindow.getOutgoingCapacity();
+    }
+
+    @Override
+    public int getRemainingOutgoingCapacity() {
+        return outgoingWindow.getRemainingOutgoingCapacity();
+    }
+
+    @Override
+    public Session setHandleMax(long handleMax) throws IllegalStateException {
+        checkNotOpened("Cannot set handle max on already opened Session");
+        this.localBegin.setHandleMax(handleMax);
+
+        return this;
+    }
+
+    @Override
+    public long getHandleMax() {
+        return localBegin.getHandleMax();
+    }
+
+    @Override
+    public ProtonSession setProperties(Map<Symbol, Object> properties) {
+        checkNotOpened("Cannot set Properties on already opened Session");
+
+        if (properties != null) {
+            localBegin.setProperties(new LinkedHashMap<>(properties));
+        } else {
+            localBegin.setProperties(properties);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Map<Symbol, Object> getProperties() {
+        if (localBegin.getProperties() != null) {
+            return Collections.unmodifiableMap(localBegin.getProperties());
+        }
+
+        return null;
+    }
+
+    @Override
+    public ProtonSession setOfferedCapabilities(Symbol... capabilities) {
+        checkNotOpened("Cannot set Offered Capabilities on already opened Session");
+
+        if (capabilities != null) {
+            localBegin.setOfferedCapabilities(Arrays.copyOf(capabilities, capabilities.length));
+        } else {
+            localBegin.setOfferedCapabilities(capabilities);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Symbol[] getOfferedCapabilities() {
+        if (localBegin.getOfferedCapabilities() != null) {
+            return Arrays.copyOf(localBegin.getOfferedCapabilities(), localBegin.getOfferedCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public ProtonSession setDesiredCapabilities(Symbol... capabilities) {
+        checkNotOpened("Cannot set Desired Capabilities on already opened Session");
+
+        if (capabilities != null) {
+            localBegin.setDesiredCapabilities(Arrays.copyOf(capabilities, capabilities.length));
+        } else {
+            localBegin.setDesiredCapabilities(capabilities);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Symbol[] getDesiredCapabilities() {
+        if (localBegin.getDesiredCapabilities() != null) {
+            return Arrays.copyOf(localBegin.getDesiredCapabilities(), localBegin.getDesiredCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Set<Link<?>> links() {
+        return Collections.unmodifiableSet(allLinks());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Set<ProtonSender> senders() {
+        final Set<ProtonSender> result;
+
+        if (senderByNameMap.isEmpty()) {
+            result = Collections.EMPTY_SET;
+        } else {
+            result = new HashSet<>(senderByNameMap.values());
+        }
+
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Set<ProtonReceiver> receivers() {
+        final Set<ProtonReceiver> result;
+
+        if (receiverByNameMap.isEmpty()) {
+            result = Collections.EMPTY_SET;
+        } else {
+            result = new HashSet<>(receiverByNameMap.values());
+        }
+
+        return result;
+    }
+
+    //----- View of the remote end of this endpoint
+
+    @Override
+    public boolean isRemotelyOpen() {
+        return getRemoteState() == SessionState.ACTIVE;
+    }
+
+    @Override
+    public boolean isRemotelyClosed() {
+        return getRemoteState() == SessionState.CLOSED;
+    }
+
+    @Override
+    public Symbol[] getRemoteOfferedCapabilities() {
+        if (remoteBegin != null && remoteBegin.getOfferedCapabilities() != null) {
+            return Arrays.copyOf(remoteBegin.getOfferedCapabilities(), remoteBegin.getOfferedCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Symbol[] getRemoteDesiredCapabilities() {
+        if (remoteBegin != null && remoteBegin.getDesiredCapabilities() != null) {
+            return Arrays.copyOf(remoteBegin.getDesiredCapabilities(), remoteBegin.getDesiredCapabilities().length);
+        }
+
+        return null;
+    }
+
+    @Override
+    public Map<Symbol, Object> getRemoteProperties() {
+        if (remoteBegin != null && remoteBegin.getProperties() != null) {
+            return Collections.unmodifiableMap(remoteBegin.getProperties());
+        }
+
+        return null;
+    }
+
+    //----- Session factory methods for Sender and Receiver
+
+    @Override
+    public ProtonSender sender(String name) {
+        checkSessionClosed("Cannot create new Sender from closed Session");
+
+        ProtonSender sender = senderByNameMap.get(name);
+
+        if (sender == null) {
+            sender = new ProtonSender(this, name);
+            senderByNameMap.put(name, sender);
+        }
+
+        return sender;
+    }
+
+    @Override
+    public ProtonReceiver receiver(String name) {
+        checkSessionClosed("Cannot create new Receiver from closed Session");
+
+        ProtonReceiver receiver = receiverByNameMap.get(name);
+
+        if (receiver == null) {
+            receiver = new ProtonReceiver(this, name);
+            receiverByNameMap.put(name, receiver);
+        }
+
+        return receiver;
+    }
+
+    @Override
+    public TransactionController coordinator(String name) throws IllegalStateException {
+        checkSessionClosed("Cannot create new TransactionController from closed Session");
+
+        ProtonSender sender = senderByNameMap.get(name);
+
+        if (sender == null) {
+            sender = new ProtonSender(this, name);
+            senderByNameMap.put(name, sender);
+        }
+
+        return new ProtonTransactionController(sender);
+    }
+
+    //----- Event handler registration for this Session
+
+    @Override
+    public ProtonSession senderOpenHandler(EventHandler<Sender> remoteSenderOpenEventHandler) {
+        this.remoteSenderOpenEventHandler = remoteSenderOpenEventHandler;
+        return this;
+    }
+
+    EventHandler<Sender> senderOpenEventHandler() {
+        return remoteSenderOpenEventHandler;
+    }
+
+    @Override
+    public ProtonSession receiverOpenHandler(EventHandler<Receiver> remoteReceiverOpenEventHandler) {
+        this.remoteReceiverOpenEventHandler = remoteReceiverOpenEventHandler;
+        return this;
+    }
+
+    EventHandler<Receiver> receiverOpenEventHandler() {
+        return remoteReceiverOpenEventHandler;
+    }
+
+    @Override
+    public ProtonSession transactionManagerOpenHandler(EventHandler<TransactionManager> remoteTxnManagerOpenEventHandler) {
+        this.remoteTxnManagerOpenEventHandler = remoteTxnManagerOpenEventHandler;
+        return this;
+    }
+
+    EventHandler<TransactionManager> transactionManagerOpenHandler() {
+        return remoteTxnManagerOpenEventHandler;
+    }
+
+    //----- Respond to Connection and Engine state changes
+
+    void handleConnectionLocallyClosed(ProtonConnection protonConnection) {
+        allLinks().forEach(link -> link.handleConnectionLocallyClosed(connection));
+    }
+
+    void handleConnectionRemotelyClosed(ProtonConnection protonConnection) {
+        allLinks().forEach(link -> link.handleConnectionRemotelyClosed(connection));
+    }
+
+    void handleEngineShutdown(ProtonEngine protonEngine) {
+        try {
+            fireEngineShutdown();
+        } catch (Throwable ingore) {}
+
+        allLinks().forEach(link -> link.handleEngineShutdown(protonEngine));
+    }
+
+    //----- Handle incoming performatives
+
+    void remoteBegin(Begin begin, int channel) {
+        remoteBegin = begin;
+        localBegin.setRemoteChannel(channel);
+        remoteState = SessionState.ACTIVE;
+        incomingWindow.handleBegin(begin);
+        outgoingWindow.handleBegin(begin);
+
+        if (isLocallyOpen()) {
+            fireRemoteOpen();
+        }
+    }
+
+    void remoteEnd(End end, int channel) {
+        allLinks().forEach(link -> link.handleSessionRemotelyClosed(this));
+
+        setRemoteCondition(end.getError());
+        remoteState = SessionState.CLOSED;
+
+        fireRemoteClose();
+    }
+
+    void remoteAttach(Attach attach, int channel) {
+        if (validateHandleMaxCompliance(attach)) {
+            if (remoteLinks.containsKey((int) attach.getHandle())) {
+                setCondition(new ErrorCondition(SessionError.HANDLE_IN_USE, "Attach received with handle that is already in use")).close();
+                return;
+            }
+
+            if (!attach.hasInitialDeliveryCount() && attach.getRole() == Role.SENDER) {
+                throw new ProtocolViolationException("Sending peer attach had no initial delivery count");
+            }
+
+            ProtonLink<?> link = findMatchingPendingLinkOpen(attach);
+            if (link == null) {
+                link = attach.getRole() == Role.RECEIVER ? sender(attach.getName()) : receiver(attach.getName());
+            }
+
+            remoteLinks.put((int) attach.getHandle(), link);
+
+            link.remoteAttach(attach);
+        }
+    }
+
+    void remoteDetach(Detach detach, int channel) {
+        final ProtonLink<?> link = remoteLinks.remove((int) detach.getHandle());
+        if (link == null) {
+            getEngine().engineFailed(new ProtocolViolationException(
+                "Received uncorrelated handle on Detach from remote: " + channel));
+            return;
+        }
+
+        // Ensure that tracked links get cleared at some point as we don't currently have the concept
+        // of link free APIs to put this onto the user to manage.
+        if (link.isLocallyClosed() || link.isLocallyDetached()) {
+            if (link.isReceiver()) {
+                receiverByNameMap.remove(link.getName());
+            } else {
+                senderByNameMap.remove(link.getName());
+            }
+         }
+
+        link.remoteDetach(detach);
+    }
+
+    void remoteFlow(Flow flow, int channel) {
+        final boolean previousSessionWritable = outgoingWindow.isSendable();
+
+        // Session level flow processing.
+        incomingWindow.handleFlow(flow);
+        outgoingWindow.handleFlow(flow);
+
+        if (flow.hasHandle()) {
+            final ProtonLink<?> link = remoteLinks.get((int) flow.getHandle());
+            if (link == null) {
+                getEngine().engineFailed(new ProtocolViolationException(
+                    "Received uncorrelated handle on Flow from remote: " + channel));
+                return;
+            }
+
+            link.remoteFlow(flow);
+        } else {
+            handleSessionOnlyFlow(flow, previousSessionWritable);
+        }
+    }
+
+    private void handleSessionOnlyFlow(Flow flow, boolean previousSessionWritable) {
+        if (previousSessionWritable != outgoingWindow.isSendable()) {
+            for (ProtonSender sender : senders()) {
+                sender.handleSessionCreditStateUpdate(outgoingWindow);
+
+                if (previousSessionWritable == outgoingWindow.isSendable()) {
+                    break;
+                }
+            }
+        }
+
+        if (flow.getEcho()) {
+            // Auto respond to session level echo requests as there's not an event point at the
+            // moment that would otherwise allow a response.
+            writeFlow(null);
+        }
+    }
+
+    void remoteTransfer(Transfer transfer, ProtonBuffer payload, int channel) {
+        final ProtonLink<?> link = remoteLinks.get((int) transfer.getHandle());
+        if (link == null) {
+            getEngine().engineFailed(new ProtocolViolationException(
+                "Received uncorrelated handle on Transfer from remote: " + channel));
+        } else if (!link.isRemotelyOpen()) {
+            getEngine().engineFailed(new ProtocolViolationException("Received Transfer for detached Receiver: " + link));
+        } else {
+            incomingWindow.handleTransfer(link, transfer, payload);
+        }
+    }
+
+    void remoteDispsotion(Disposition disposition, int channel) {
+        if (disposition.getRole() == Role.RECEIVER) {
+            outgoingWindow.handleDisposition(disposition);
+        } else {
+            incomingWindow.handleDisposition(disposition);
+        }
+    }
+
+    //----- Internal implementation
+
+    ProtonSessionOutgoingWindow getOutgoingWindow() {
+        return outgoingWindow;
+    }
+
+    ProtonSessionIncomingWindow getIncomingWindow() {
+        return incomingWindow;
+    }
+
+    boolean wasLocalBeginSent() {
+        return localBeginSent;
+    }
+
+    boolean wasLocalEndSent() {
+        return localEndSent;
+    }
+
+    void freeLink(ProtonLink<?> linkToFree) {
+        freeLocalHandle(linkToFree.getHandle());
+
+        if (linkToFree.isRemotelyClosed() || linkToFree.isRemotelyDetached()) {
+            if (linkToFree.isReceiver()) {
+                receiverByNameMap.remove(linkToFree.getName());
+            } else {
+                senderByNameMap.remove(linkToFree.getName());
+            }
+        }
+    }
+
+    void writeFlow(ProtonLink<?> link) {
+        cachedFlow.reset();
+
+        // (AmqpSpec:Section 2.7.4) This value must not be set if the remote begin has not been received.
+        if (remoteBegin != null) {
+            cachedFlow.setNextIncomingId(getIncomingWindow().getNextIncomingId());
+        }
+
+        cachedFlow.setNextOutgoingId(getOutgoingWindow().getNextOutgoingId());
+        cachedFlow.setIncomingWindow(getIncomingWindow().getIncomingWindow());
+        cachedFlow.setOutgoingWindow(getOutgoingWindow().getOutgoingWindow());
+
+        if (link != null) {
+            link.decorateOutgoingFlow(cachedFlow);
+        }
+
+        getEngine().fireWrite(cachedFlow, localChannel);
+    }
+
+    private void checkNotOpened(String errorMessage) {
+        if (localState.ordinal() > SessionState.IDLE.ordinal()) {
+            throw new IllegalStateException(errorMessage);
+        }
+    }
+
+    private void checkConnectionClosed() {
+        if (connection.getState() == ConnectionState.CLOSED || connection.getRemoteState() == ConnectionState.CLOSED) {
+             throw new IllegalStateException("Cannot open a Session from a Connection that is already closed");
+        }
+    }
+
+    private void checkSessionClosed(String errorMessage) {
+        if (isLocallyClosed() || isRemotelyClosed()) {
+             throw new IllegalStateException(errorMessage);
+        }
+    }
+
+    private ProtonLink<?> findMatchingPendingLinkOpen(Attach remoteAttach) {
+        for (ProtonLink<?> link : senderByNameMap.values()) {
+            if (link.getName().equals(remoteAttach.getName()) &&
+                link.getRemoteState() == LinkState.IDLE &&
+                link.getRole() != remoteAttach.getRole()) {
+
+                return link;
+            }
+        }
+
+        for (ProtonLink<?> link : receiverByNameMap.values()) {
+            if (link.getName().equals(remoteAttach.getName()) &&
+                link.getRemoteState() == LinkState.IDLE &&
+                link.getRole() != remoteAttach.getRole()) {
+
+                return link;
+            }
+        }
+
+        return null;
+    }
+
+    private boolean validateHandleMaxCompliance(Attach remoteAttach) {
+        final long remoteHandle = remoteAttach.getHandle();
+        if (localBegin.getHandleMax() < remoteHandle) {
+            // The handle-max value is the highest handle value that can be used on the session. A peer MUST
+            // NOT attempt to attach a link using a handle value outside the range that its partner can handle.
+            // A peer that receives a handle outside the supported range MUST close the connection with the
+            // framing-error error-code.
+            ErrorCondition condition = new ErrorCondition(ConnectionError.FRAMING_ERROR, "Session handle-max exceeded");
+            connection.setCondition(condition);
+            connection.close();
+
+            return false;
+        }
+
+        return true;
+    }
+
+    void trySyncLocalStateWithRemote() {
+        switch (getState()) {
+            case IDLE:
+                return;
+            case ACTIVE:
+                checkIfBeginShouldBeSent();
+                break;
+            case CLOSED:
+                checkIfBeginShouldBeSent();
+                checkIfEndShouldBeSent();
+                break;
+            default:
+                throw new IllegalStateException("Session is in unknown state and cannot proceed");
+        }
+    }
+
+    private void checkIfBeginShouldBeSent() {
+        if (!wasLocalBeginSent()) {
+            if (connection.isLocallyOpen() && connection.wasLocalOpenSent()) {
+                fireSessionBegin();
+            }
+        }
+    }
+
+    private void checkIfEndShouldBeSent() {
+        if (!wasLocalEndSent()) {
+            if (connection.isLocallyOpen() && connection.wasLocalOpenSent() && !engine.isShutdown()) {
+                fireSessionEnd();
+            }
+        }
+    }
+
+    private void fireSessionBegin() {
+        connection.getEngine().fireWrite(localBegin, localChannel);
+        localBeginSent = true;
+        allLinks().forEach(link -> link.trySyncLocalStateWithRemote());
+    }
+
+    private void fireSessionEnd() {
+        connection.getEngine().fireWrite(new End().setError(getCondition()), localChannel);
+        localEndSent = true;
+        connection.freeLocalChannel(localChannel);
+    }
+
+    long findFreeLocalHandle(ProtonLink<?> link) {
+        for (long i = 0; i <= localBegin.getHandleMax(); ++i) {
+            if (!localLinks.containsKey((int) i)) {
+                localLinks.put((int) i, link);
+                return i;
+            }
+        }
+
+        throw new IllegalStateException("no local handle available for allocation");
+    }
+
+    @SuppressWarnings("unchecked")
+    private Set<ProtonLink<?>> allLinks() {
+        final Set<ProtonLink<?>> result;
+
+        if (senderByNameMap.isEmpty() && receiverByNameMap.isEmpty()) {
+            return Collections.EMPTY_SET;
+        } else {
+            result = new HashSet<>(senderByNameMap.size());
+
+            result.addAll(senderByNameMap.values());
+            result.addAll(receiverByNameMap.values());
+        }
+
+        return result;
+    }
+
+    private void freeLocalHandle(long localHandle) {
+        if (localHandle > ProtonConstants.HANDLE_MAX) {
+            throw new IllegalArgumentException("Specified local handle is out of range: " + localHandle);
+        }
+
+        localLinks.remove((int) localHandle);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionIncomingWindow.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionIncomingWindow.java
new file mode 100644
index 0000000..e69e495
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionIncomingWindow.java
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.util.SequenceNumber;
+import org.apache.qpid.protonj2.engine.util.SplayMap;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Tracks the incoming window and provides management of that window in relation to receiver links.
+ * <p>
+ * The incoming window decreases as {@link Transfer} frames arrive and is replenished when the user reads the
+ * bytes received in the accumulated payload of a delivery.  The window is expanded by sending a {@link Flow}
+ * frame to the remote with an updated incoming window value at configured intervals based on reads from the
+ * pending deliveries.
+ */
+public class ProtonSessionIncomingWindow {
+
+    private static final long DEFAULT_WINDOW_SIZE = Integer.MAX_VALUE; // biggest legal value
+
+    private final ProtonSession session;
+    private final ProtonEngine engine;
+
+    // User configured incoming capacity for the session used to compute the incoming window
+    private int incomingCapacity = 0;
+
+    // Computed incoming window based on the incoming capacity minus bytes not yet read from deliveries.
+    private long incomingWindow = 0;
+
+    // Tracks the next expected incoming transfer ID from the remote
+    private long nextIncomingId = 0;
+
+    // Tracks the most recent delivery Id for validation against the next incoming delivery
+    private SequenceNumber lastDeliveryid;
+
+    private long maxFrameSize;
+    private long incomingBytes;
+
+    private SplayMap<ProtonIncomingDelivery> unsettled = new SplayMap<>();
+
+    public ProtonSessionIncomingWindow(ProtonSession session) {
+        this.session = session;
+        this.engine = session.getConnection().getEngine();
+        this.maxFrameSize = session.getConnection().getMaxFrameSize();
+    }
+
+    public void setIncomingCapaity(int incomingCapacity) {
+        this.incomingCapacity = incomingCapacity;
+    }
+
+    public int getIncomingCapacity() {
+        return incomingCapacity;
+    }
+
+    public int getRemainingIncomingCapacity() {
+        // TODO: This is linked to below update of capacity which also needs more attention.
+        if (incomingCapacity <= 0 || maxFrameSize == UnsignedInteger.MAX_VALUE.longValue()) {
+            return (int) DEFAULT_WINDOW_SIZE;
+        } else {
+            return (int) (incomingCapacity - incomingBytes);
+        }
+    }
+
+    /**
+     * Initialize the session level window values on the outbound Begin
+     *
+     * @param begin
+     *      The {@link Begin} performative that is about to be sent.
+     *
+     * @return the configured performative
+     */
+    Begin configureOutbound(Begin begin) {
+        // Update as it might have changed if session created before connection open() called.
+        this.maxFrameSize = session.getConnection().getMaxFrameSize();
+
+        return begin.setIncomingWindow(updateIncomingWindow());
+    }
+
+    /**
+     * Update the session level window values based on remote information.
+     *
+     * @param begin
+     *      The {@link Begin} performative received from the remote.
+     *
+     * @return the given performative for chaining
+     */
+    Begin handleBegin(Begin begin) {
+        if (begin.hasNextOutgoingId()) {
+            this.nextIncomingId = begin.getNextOutgoingId();
+        }
+
+        return begin;
+    }
+
+    /**
+     * Update the session window state based on an incoming {@link Flow} performative
+     *
+     * @param flow
+     *      the incoming {@link Flow} performative to process.
+     */
+    Flow handleFlow(Flow flow) {
+        return flow;
+    }
+
+    /**
+     * Update the session window state based on an incoming {@link Transfer} performative
+     *
+     * @param transfer
+     *      the incoming {@link Transfer} performative to process.
+     * @param payload
+     *      the payload that was transmitted with the incoming {@link Transfer}
+     */
+    Transfer handleTransfer(ProtonLink<?> link, Transfer transfer, ProtonBuffer payload) {
+        incomingBytes += payload != null ? payload.getReadableBytes() : 0;
+        incomingWindow--;
+        nextIncomingId++;
+
+        ProtonIncomingDelivery delivery = link.remoteTransfer(transfer, payload);
+        if (!delivery.isRemotelySettled() && delivery.isFirstTransfer()) {
+            unsettled.put((int) delivery.getDeliveryId(), delivery);
+        }
+
+        return transfer;
+    }
+
+    /**
+     * Update the state of any received Transfers that are indicated in the disposition
+     * with the state information conveyed therein.
+     *
+     * @param disposition
+     *      The {@link Disposition} performative to process
+     *
+     * @return the {@link Disposition}
+     */
+    Disposition handleDisposition(Disposition disposition) {
+        final int first = (int) disposition.getFirst();
+
+        if (disposition.hasLast() && disposition.getLast() != first) {
+            handleRangedDisposition(disposition);
+        } else {
+            final ProtonIncomingDelivery delivery = disposition.getSettled() ?
+                unsettled.remove(first) : unsettled.get(first);
+
+            if (delivery != null) {
+                delivery.getLink().remoteDisposition(disposition, delivery);
+            }
+        }
+
+        return disposition;
+    }
+
+    private void handleRangedDisposition(Disposition disposition) {
+        final int first = (int) disposition.getFirst();
+        final int last = (int) disposition.getLast();
+        final boolean settled = disposition.getSettled();
+
+        int index = first;
+        ProtonIncomingDelivery delivery;
+
+        // TODO: If SplayMap gets a subMap that works we could get the ranged view which would
+        //       be more efficient.
+        do {
+            delivery = settled ? unsettled.remove(index) : unsettled.get(index);
+
+            if (delivery != null) {
+                delivery.getLink().remoteDisposition(disposition, delivery);
+            }
+        } while (index++ != last);
+    }
+
+    long updateIncomingWindow() {
+        // TODO - need to revisit this logic and decide on sane cutoff for capacity restriction.
+        if (incomingCapacity <= 0 || maxFrameSize == UnsignedInteger.MAX_VALUE.longValue()) {
+            incomingWindow = DEFAULT_WINDOW_SIZE;
+        } else {
+            // TODO - incomingWindow = Integer.divideUnsigned(incomingCapacity - incomingBytes, maxFrameSize);
+            incomingWindow = (incomingCapacity - incomingBytes) / maxFrameSize;
+        }
+
+        return incomingWindow;
+    }
+
+    void writeFlow(ProtonReceiver link) {
+        updateIncomingWindow();
+        session.writeFlow(link);
+    }
+
+    //----- Access to internal state useful for tests
+
+    public long getIncomingBytes() {
+        return incomingBytes;
+    }
+
+    public long getNextIncomingId() {
+        return nextIncomingId;
+    }
+
+    public long getIncomingWindow() {
+        return incomingWindow;
+    }
+
+    //----- Handle sender link actions in the session window context
+
+    private final Disposition cachedDisposition = new Disposition();
+
+    void processDisposition(ProtonReceiver receiver, ProtonIncomingDelivery delivery) {
+        if (!delivery.isRemotelySettled()) {
+            // Would only be tracked if not already remotely settled.
+            if (delivery.isSettled()) {
+                unsettled.remove((int) delivery.getDeliveryId());
+            }
+
+            cachedDisposition.reset();
+            cachedDisposition.setFirst(delivery.getDeliveryId());
+            cachedDisposition.setRole(Role.RECEIVER);
+            cachedDisposition.setSettled(delivery.isSettled());
+            cachedDisposition.setState(delivery.getState());
+
+            engine.fireWrite(cachedDisposition, session.getLocalChannel());
+        }
+    }
+
+    void deliveryRead(ProtonIncomingDelivery delivery, int bytesRead) {
+        this.incomingBytes -= bytesRead;
+        if (incomingWindow == 0) {
+            writeFlow(delivery.getLink());
+        }
+    }
+
+    void validateNextDeliveryId(long deliveryId) {
+        if (lastDeliveryid == null) {
+            lastDeliveryid = new SequenceNumber((int) deliveryId);
+        } else {
+            int previousId = lastDeliveryid.intValue();
+            if (lastDeliveryid.increment().compareTo((int) deliveryId) != 0) {
+                session.getConnection().getEngine().engineFailed(
+                    new ProtocolViolationException("Expected delivery-id " + previousId + ", got " + deliveryId));
+            }
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionOutgoingWindow.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionOutgoingWindow.java
new file mode 100644
index 0000000..0c1bcaa
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionOutgoingWindow.java
@@ -0,0 +1,367 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.Set;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.util.SplayMap;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Performative;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * Holds Session level credit window information for outgoing transfers from this
+ * Session.  The window is constrained by the remote incoming capacity restrictions
+ * or if present outgoing restrictions on pending transfers.
+ */
+public class ProtonSessionOutgoingWindow {
+
+    private final ProtonSession session;
+    private final ProtonEngine engine;
+    private final int localChannel;
+
+    // This is used for the delivery-id actually stamped in each transfer frame of a given message delivery.
+    private int outgoingDeliveryId = 0;
+
+    // Conceptual outgoing Transfer ID value.
+    private int nextOutgoingId = 0;
+
+    // Track outgoing windowing state information in order to stop outgoing writes if the high
+    // water mark is hit and restart later once the low water mark is hit.  When outgoing capacity
+    // is at the default -1 value then no real limit is applied.  If set to zero no writes are allowed.
+    private int outgoingCapacity = -1;
+    private int outgoingWindowHighWaterMark = Integer.MAX_VALUE;
+    private int outgoingWindowLowWaterMark = Integer.MAX_VALUE / 2;
+    private int pendingOutgoingWrites;
+    private boolean writeable;
+
+    private long remoteIncomingWindow;
+    private int remoteNextIncomingId = nextOutgoingId;
+
+    private final SplayMap<ProtonOutgoingDelivery> unsettled = new SplayMap<>();
+
+    public ProtonSessionOutgoingWindow(ProtonSession session) {
+        this.session = session;
+        this.engine = session.getConnection().getEngine();
+        this.localChannel = session.getLocalChannel();
+    }
+
+    /**
+     * Initialize the session level window values on the outbound Begin
+     *
+     * @param begin
+     *      The {@link Begin} performative that is about to be sent.
+     *
+     * @return the configured performative
+     */
+    Begin configureOutbound(Begin begin) {
+        begin.setNextOutgoingId(getNextOutgoingId());
+        begin.setOutgoingWindow(getOutgoingWindow());
+
+        updateOutgoingWindowState();
+
+        return begin;
+    }
+
+    int getAndIncrementNextDeliveryId() {
+        return outgoingDeliveryId++;
+    }
+
+    void setOutgoingCapacity(int outgoingCapacity) {
+        this.outgoingCapacity = outgoingCapacity;
+        updateOutgoingWindowState();
+    }
+
+    int getOutgoingCapacity() {
+        return outgoingCapacity;
+    }
+
+    int getRemainingOutgoingCapacity() {
+        // If set to lower value after some writes are pending this calculation could go negative which we don't want
+        // so ensure it never drops below zero.  Then limit the max value to max positive value and hold there
+        // as it being more than that is a fairly pointless value to try and convey.
+        final int allowedWrites = Math.max(0, outgoingWindowHighWaterMark - pendingOutgoingWrites);
+        final int remaining = (int) (allowedWrites * session.getEngine().configuration().getOutboundMaxFrameSize());
+
+        if (outgoingCapacity < 0 || remaining < 0) {
+            return Integer.MAX_VALUE;
+        } else {
+            return remaining;
+        }
+    }
+
+    boolean isSendable() {
+        return writeable;
+    }
+
+    private void updateOutgoingWindowState() {
+        final boolean oldWritable = writeable;
+        final int maxFrameSize = (int) session.getEngine().configuration().getOutboundMaxFrameSize();
+
+        if (outgoingCapacity == 0) {
+            // At a setting of zero outgoing writes is manually disabled until elevated again to > 0
+            outgoingWindowHighWaterMark = outgoingWindowLowWaterMark = 0;
+            writeable = false;
+        } else if (outgoingCapacity > 0) {
+            // The local end is writable here if the current pending writes count is below the low water
+            // mark and also if there is remote incoming window to allow more write.
+            outgoingWindowHighWaterMark = Math.max(1, outgoingCapacity / maxFrameSize);
+            outgoingWindowLowWaterMark = outgoingWindowHighWaterMark / 2;
+            writeable = pendingOutgoingWrites <= outgoingWindowLowWaterMark && remoteIncomingWindow > 0;
+        } else {
+            // User disabled outgoing windowing so reset state to reflect that we are not
+            // enforcing any limit from now on, at least not any sane limit.
+            outgoingWindowHighWaterMark = Integer.MAX_VALUE;
+            outgoingWindowLowWaterMark = Integer.MAX_VALUE / 2;
+            writeable = remoteIncomingWindow > 0;
+        }
+
+        if (!oldWritable && writeable) {
+            Set<ProtonSender> senders = session.senders();
+            for (ProtonSender sender : senders) {
+                sender.handleSessionCreditStateUpdate(this);
+                if (!writeable) {
+                    break;
+                }
+            }
+        }
+    }
+
+    private void handleOutgoingFrameWriteComplete() {
+        pendingOutgoingWrites = Math.max(0, --pendingOutgoingWrites);
+
+        if (!writeable && (writeable = pendingOutgoingWrites <= outgoingWindowLowWaterMark && remoteIncomingWindow > 0)) {
+            Set<ProtonSender> senders = session.senders();
+            for (ProtonSender sender : senders) {
+                sender.handleSessionCreditStateUpdate(this);
+                if (!writeable) {
+                    break;
+                }
+            }
+        }
+    }
+
+    //----- Handle incoming performatives relevant to the session.
+
+    /**
+     * Update the session level window values based on remote information.
+     *
+     * @param begin
+     *      The {@link Begin} performative received from the remote.
+     *
+     * @return the given performative for chaining
+     */
+    Begin handleBegin(Begin begin) {
+        remoteIncomingWindow = begin.getIncomingWindow();
+        return begin;
+    }
+
+    /**
+     * Update the session window state based on an incoming {@link Flow} performative
+     *
+     * @param flow
+     *      the incoming {@link Flow} performative to process.
+     */
+    Flow handleFlow(Flow flow) {
+        if (flow.hasNextIncomingId()) {
+            remoteNextIncomingId = (int) flow.getNextIncomingId();
+            remoteIncomingWindow = (remoteNextIncomingId + flow.getIncomingWindow()) - nextOutgoingId;
+        } else {
+            remoteIncomingWindow = flow.getIncomingWindow();
+        }
+
+        writeable = remoteIncomingWindow > 0 && pendingOutgoingWrites <= outgoingWindowLowWaterMark;
+
+        return flow;
+    }
+
+    /**
+     * Update the session window state based on an incoming {@link Transfer} performative
+     *
+     * @param transfer
+     *      the incoming {@link Transfer} performative to process.
+     */
+    Transfer handleTransfer(Transfer transfer, ProtonBuffer payload) {
+        return transfer;
+    }
+
+    /**
+     * Update the state of any sent Transfers that are indicated in the disposition
+     * with the state information conveyed therein.
+     *
+     * @param disposition
+     *      The {@link Disposition} performative to process
+     *
+     * @return the {@link Disposition}
+     */
+    Disposition handleDisposition(Disposition disposition) {
+        final int first = (int) disposition.getFirst();
+
+        if (disposition.hasLast() && disposition.getLast() != first) {
+            handleRangedDisposition(disposition);
+        } else {
+            final ProtonOutgoingDelivery delivery = disposition.getSettled() ?
+                unsettled.remove(first) : unsettled.get(first);
+
+            if (delivery != null) {
+                delivery.getLink().remoteDisposition(disposition, delivery);
+            }
+        }
+
+        return disposition;
+    }
+
+    private void handleRangedDisposition(Disposition disposition) {
+        final int first = (int) disposition.getFirst();
+        final int last = (int) disposition.getLast();
+        final boolean settled = disposition.getSettled();
+
+        int index = first;
+        ProtonOutgoingDelivery delivery;
+
+        // TODO: If SplayMap gets a subMap that works we could get the ranged view which would
+        //       be more efficient.
+        do {
+            delivery = settled ? unsettled.remove(index) : unsettled.get(index);
+
+            if (delivery != null) {
+                delivery.getLink().remoteDisposition(disposition, delivery);
+            }
+        } while (index++ != last);
+    }
+
+    //----- Handle sender link actions in the session window context
+
+    private final Disposition cachedDisposition = new Disposition();
+    private final Transfer cachedTransfer = new Transfer();
+
+    private void handlePayloadToLargeRequiresSplitFrames(Performative performative) {
+        cachedTransfer.setMore(true);
+    }
+
+    boolean processSend(ProtonSender sender, ProtonOutgoingDelivery delivery, ProtonBuffer payload, boolean complete) {
+        // For a transfer that hasn't completed but has no bytes in the final transfer write we want
+        // to allow a transfer to go out with the more flag as false.
+
+        if (!delivery.isSettled()) {
+            unsettled.put((int) delivery.getDeliveryId(), delivery);
+        }
+
+        try {
+            cachedTransfer.setDeliveryId(delivery.getDeliveryId());
+            if (delivery.getMessageFormat() != 0) {
+                cachedTransfer.setMessageFormat(delivery.getMessageFormat());
+            } else {
+                cachedTransfer.clearMessageFormat();
+            }
+            cachedTransfer.setHandle(sender.getHandle());
+            cachedTransfer.setSettled(delivery.isSettled());
+            cachedTransfer.setState(delivery.getState());
+
+            do {
+                // Update session window tracking for each transfer that ends up being sent.
+                ++nextOutgoingId;
+                ++pendingOutgoingWrites;
+                --remoteIncomingWindow;
+
+                writeable = pendingOutgoingWrites < outgoingWindowHighWaterMark && remoteIncomingWindow > 0;
+
+                // Only the first transfer requires the delivery tag, afterwards we can omit it for efficiency.
+                if (delivery.getTransferCount() == 0) {
+                    cachedTransfer.setDeliveryTag(delivery.getTag());
+                } else {
+                    cachedTransfer.setDeliveryTag((DeliveryTag) null);
+                }
+                cachedTransfer.setMore(!complete);
+
+                OutgoingAMQPEnvelope frame = engine.wrap(cachedTransfer, localChannel, payload);
+
+                frame.setPayloadToLargeHandler(this::handlePayloadToLargeRequiresSplitFrames);
+                frame.setFrameWriteCompletionHandler(this::handleOutgoingFrameWriteComplete);
+
+                engine.fireWrite(frame);
+
+                delivery.afterTransferWritten();
+            } while (payload != null && payload.isReadable() && isSendable());
+        } finally {
+            cachedTransfer.reset();
+        }
+
+        return isSendable();
+    }
+
+    void processDisposition(ProtonSender sender, ProtonOutgoingDelivery delivery) {
+        // Would only be tracked if not already remotely settled.
+        if (delivery.isSettled() && !delivery.isRemotelySettled()) {
+            unsettled.remove((int) delivery.getDeliveryId());
+        }
+
+        if (!delivery.isRemotelySettled()) {
+            cachedDisposition.setFirst(delivery.getDeliveryId());
+            cachedDisposition.setRole(Role.SENDER);
+            cachedDisposition.setSettled(delivery.isSettled());
+            cachedDisposition.setState(delivery.getState());
+
+            try {
+                engine.fireWrite(cachedDisposition, session.getLocalChannel());
+            } finally {
+                cachedDisposition.reset();
+            }
+        }
+    }
+
+    void processAbort(ProtonSender sender, ProtonOutgoingDelivery delivery) {
+        cachedTransfer.setDeliveryId(delivery.getDeliveryId());
+        cachedTransfer.setDeliveryTag(delivery.getTag());
+        cachedTransfer.setSettled(true);
+        cachedTransfer.setAborted(true);
+        cachedTransfer.setHandle(sender.getHandle());
+
+        // Ensure we don't track the aborted delivery any longer.
+        unsettled.remove((int) delivery.getDeliveryId());
+
+        try {
+            engine.fireWrite(cachedTransfer, session.getLocalChannel());
+        } finally {
+            cachedTransfer.reset();
+        }
+    }
+
+    //----- Access to internal state useful for tests
+
+    int getNextOutgoingId() {
+        return nextOutgoingId;
+    }
+
+    long getOutgoingWindow() {
+        return Integer.MAX_VALUE;
+    }
+
+    int getRemoteNextIncomingId() {
+        return remoteNextIncomingId;
+    }
+
+    long getRemoteIncomingWindow() {
+        return remoteIncomingWindow;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransaction.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransaction.java
new file mode 100644
index 0000000..a41410a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransaction.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import org.apache.qpid.protonj2.engine.Endpoint;
+import org.apache.qpid.protonj2.engine.Transaction;
+import org.apache.qpid.protonj2.engine.TransactionController;
+import org.apache.qpid.protonj2.engine.TransactionManager;
+import org.apache.qpid.protonj2.engine.TransactionState;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * Base {@link Transaction} implementation that provides the basic functionality needed
+ * to manage the {@link Transaction} that it represents.
+ *
+ * @param <E> The parent type for this {@link Transaction}
+ */
+public abstract class ProtonTransaction<E extends Endpoint<?>> implements Transaction<E> {
+
+    private TransactionState state = TransactionState.IDLE;
+    private DischargeState dischargeState = DischargeState.NONE;
+    private ErrorCondition condition;
+    private Binary txnId;
+
+    private ProtonAttachments attachments;
+    private Object linkedResource;
+
+    @Override
+    public TransactionState getState() {
+        return state;
+    }
+
+    ProtonTransaction<E> setState(TransactionState state) {
+        this.state = state;
+        return this;
+    }
+
+    @Override
+    public boolean isDeclared() {
+        return state.ordinal() == TransactionState.DECLARED.ordinal();
+    }
+
+    @Override
+    public boolean isDischarged() {
+        return state.ordinal() == TransactionState.DISCHARGED.ordinal();
+    }
+
+    @Override
+    public boolean isFailed() {
+        return state.ordinal() > TransactionState.DISCHARGED.ordinal();
+    }
+
+    @Override
+    public ErrorCondition getCondition() {
+        return condition;
+    }
+
+    ProtonTransaction<E> setCondition(ErrorCondition condition) {
+        this.condition = condition;
+        return this;
+    }
+
+    @Override
+    public DischargeState getDischargeState() {
+        return dischargeState;
+    }
+
+    ProtonTransaction<E> setDischargeState(DischargeState state) {
+        this.dischargeState = state;
+        return this;
+    }
+
+    @Override
+    public Binary getTxnId() {
+        return txnId;
+    }
+
+    ProtonTransaction<E> setTxnId(Binary txnId) {
+        this.txnId = txnId;
+        return this;
+    }
+
+    @Override
+    public void setLinkedResource(Object resource) {
+        this.linkedResource = resource;
+    }
+
+    @Override
+    public Object getLinkedResource() {
+        return linkedResource;
+    }
+
+    @Override
+    public <T> T getLinkedResource(Class<T> typeClass) {
+        return typeClass.cast(linkedResource);
+    }
+
+    @Override
+    public ProtonAttachments getAttachments() {
+        return attachments == null ? attachments = new ProtonAttachments() : attachments;
+    }
+
+    /**
+     * Overridden by the {@link TransactionController} or {@link TransactionManager} that creates
+     * this {@link Transaction} instance, this method returns the parent instance that created it.
+     *
+     * @return the resource that created this {@link Transaction}.
+     */
+    @Override
+    public abstract E parent();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionController.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionController.java
new file mode 100644
index 0000000..b028791
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionController.java
@@ -0,0 +1,524 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Transaction;
+import org.apache.qpid.protonj2.engine.Transaction.DischargeState;
+import org.apache.qpid.protonj2.engine.TransactionController;
+import org.apache.qpid.protonj2.engine.TransactionState;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * {@link TransactionController} implementation that implements the abstraction
+ * around a sender link that initiates requests to {@link Declare} and to
+ * {@link Discharge} AMQP {@link Transaction} instance.
+ */
+public class ProtonTransactionController extends ProtonEndpoint<TransactionController> implements TransactionController {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonTransactionController.class);
+
+    private static final ProtonBuffer ENCODED_DECLARE;
+
+    static {
+        Encoder declareEncoder = CodecFactory.getEncoder();
+        EncoderState state = declareEncoder.newEncoderState();
+
+        ENCODED_DECLARE = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        try {
+            declareEncoder.writeObject(ENCODED_DECLARE, state, new AmqpValue<>(new Declare()));
+        } finally {
+            state.reset();
+        }
+    }
+
+    private final ProtonSender senderLink;
+    private final Encoder commandEncoder = CodecFactory.getEncoder();
+    private final ProtonBuffer encoding = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+    private final Set<Transaction<TransactionController>> transactions = new HashSet<>();
+
+    private EventHandler<Transaction<TransactionController>> declaredEventHandler;
+    private EventHandler<Transaction<TransactionController>> declareFailureEventHandler;
+    private EventHandler<Transaction<TransactionController>> dischargedEventHandler;
+    private EventHandler<Transaction<TransactionController>> dischargeFailureEventHandler;
+
+    private EventHandler<TransactionController> parentEndpointClosedEventHandler;
+
+    private List<EventHandler<TransactionController>> capacityObservers = new ArrayList<>();
+
+    public ProtonTransactionController(ProtonSender senderLink) {
+        super(senderLink.getEngine());
+
+        this.senderLink = senderLink;
+        this.senderLink.setDeliveryTagGenerator(ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator());
+        this.senderLink.deliveryStateUpdatedHandler(this::handleDeliveryRemotelyUpdated)
+                       .creditStateUpdateHandler(this::handleLinkCreditUpdated)
+                       .openHandler(this::handleSenderLinkOpened)
+                       .closeHandler(this::handleSenderLinkClosed)
+                       .parentEndpointClosedHandler(this::handleParentEndpointClosed)
+                       .localOpenHandler(this::handleSenderLinkLocallyOpened)
+                       .localCloseHandler(this::handleSenderLinkLocallyClosed)
+                       .engineShutdownHandler(this::handleEngineShutdown);
+    }
+
+    @Override
+    public ProtonSession getParent() {
+        return senderLink.getSession();
+    }
+
+    @Override
+    ProtonTransactionController self() {
+        return this;
+    }
+
+    @Override
+    public boolean hasCapacity() {
+        return senderLink.isSendable();
+    }
+
+    @Override
+    public ProtonTransactionController addCapacityAvailableHandler(EventHandler<TransactionController> handler) {
+        if (hasCapacity()) {
+            handler.handle(this);
+        } else {
+            capacityObservers.add(handler);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Collection<Transaction<TransactionController>> transactions() {
+        return Collections.unmodifiableCollection(new ArrayList<>(transactions));
+    }
+
+    @Override
+    public ProtonControllerTransaction newTransaction() {
+        ProtonControllerTransaction txn = new ProtonControllerTransaction(this);
+        transactions.add(txn);
+
+        return txn;
+    }
+
+    @Override
+    public Transaction<TransactionController> declare() {
+        if (!senderLink.isSendable()) {
+            throw new IllegalStateException("Cannot Declare due to current capicity restrictions.");
+        }
+
+        final ProtonControllerTransaction transaction = newTransaction();
+
+        declare(transaction);
+
+        return transaction;
+    }
+
+    @Override
+    public TransactionController declare(Transaction<TransactionController> transaction) {
+        if (!senderLink.isSendable()) {
+            throw new IllegalStateException("Cannot Declare due to current capicity restrictions.");
+        }
+
+        if (transaction.getState() != TransactionState.IDLE) {
+            throw new IllegalStateException("Cannot declare a transaction that has already been used previously");
+        }
+
+        if (transaction.parent() != this) {
+            throw new IllegalArgumentException("Cannot declare a transaction that was created by another controller.");
+        }
+
+        ProtonControllerTransaction protonTransaction = (ProtonControllerTransaction) transaction;
+
+        protonTransaction.setState(TransactionState.DECLARING);
+
+        OutgoingDelivery command = senderLink.next();
+
+        command.setLinkedResource(protonTransaction);
+        try {
+            command.writeBytes(ENCODED_DECLARE);
+        } finally {
+            ENCODED_DECLARE.setReadIndex(0);
+        }
+
+        return this;
+    }
+
+    @Override
+    public TransactionController discharge(Transaction<TransactionController> transaction, boolean failed) {
+        if (transaction.getState() != TransactionState.DECLARED) {
+            throw new IllegalStateException("Cannot discharge a transaction that is not currently actively declared.");
+        }
+
+        if (transaction.parent() != this) {
+            throw new IllegalArgumentException("Cannot discharge a transaction that was created by another controller.");
+        }
+
+        if (!senderLink.isSendable()) {
+            throw new IllegalStateException("Cannot discharge transaction due to current capicity restrictions.");
+        }
+
+        ProtonTransaction<TransactionController> protonTxn = (ProtonTransaction<TransactionController>) transaction;
+
+        protonTxn.setState(TransactionState.DISCHARGING);
+        protonTxn.setDischargeState(failed ? DischargeState.ROLLBACK : DischargeState.COMMIT);
+
+        Discharge discharge = new Discharge();
+        discharge.setFail(failed);
+        discharge.setTxnId(transaction.getTxnId());
+
+        commandEncoder.writeObject(encoding.clear(), commandEncoder.getCachedEncoderState(), new AmqpValue<>(discharge));
+
+        OutgoingDelivery command = senderLink.next();
+        command.setMessageFormat(0);
+        command.setLinkedResource(transaction);
+        command.writeBytes(encoding);
+
+        return this;
+    }
+
+    @Override
+    public TransactionController declaredHandler(EventHandler<Transaction<TransactionController>> declaredEventHandler) {
+        this.declaredEventHandler = declaredEventHandler;
+        return this;
+    }
+
+    @Override
+    public TransactionController declareFailureHandler(EventHandler<Transaction<TransactionController>> declareFailureEventHandler) {
+        this.declareFailureEventHandler = declareFailureEventHandler;
+        return this;
+    }
+
+    @Override
+    public TransactionController dischargedHandler(EventHandler<Transaction<TransactionController>> dischargedEventHandler) {
+        this.dischargedEventHandler = dischargedEventHandler;
+        return this;
+    }
+
+    @Override
+    public TransactionController dischargeFailureHandler(EventHandler<Transaction<TransactionController>> dischargeFailureEventHandler) {
+        this.dischargeFailureEventHandler = dischargeFailureEventHandler;
+        return this;
+    }
+
+    @Override
+    public TransactionController parentEndpointClosedHandler(EventHandler<TransactionController> handler) {
+        this.parentEndpointClosedEventHandler = handler;
+        return self();
+    }
+
+    private void fireParentEndpointClosed() {
+        if (parentEndpointClosedEventHandler != null && isLocallyOpen()) {
+            parentEndpointClosedEventHandler.handle(self());
+        }
+    }
+
+    private void fireDeclaredEvent(ProtonControllerTransaction transaction) {
+        if (declaredEventHandler != null) {
+            declaredEventHandler.handle(transaction);
+        } else {
+            LOG.debug("Transaction {} declared successfully but no handler registered to signal result", transaction);
+        }
+    }
+
+    private void fireDeclareFailureEvent(ProtonControllerTransaction transaction) {
+        if (declareFailureEventHandler != null) {
+            declareFailureEventHandler.handle(transaction);
+        } else {
+            LOG.debug("Transaction {} declare failed but no handler registered to signal result", transaction);
+        }
+    }
+
+    private void fireDischargedEvent(ProtonControllerTransaction transaction) {
+        if (dischargedEventHandler != null) {
+            dischargedEventHandler.handle(transaction);
+        } else {
+            LOG.debug("Transaction {} discharged successfully but no handler registered to signal result", transaction);
+        }
+    }
+
+    private void fireDischargeFailureEvent(ProtonControllerTransaction transaction) {
+        if (dischargeFailureEventHandler != null) {
+            dischargeFailureEventHandler.handle(transaction);
+        } else {
+            LOG.debug("Transaction {} discharge failed but no handler registered to signal result", transaction);
+        }
+    }
+
+    //----- Hand off methods for link specific elements.
+
+    @Override
+    public TransactionController open() throws IllegalStateException, EngineStateException {
+        senderLink.open();
+        return this;
+    }
+
+    @Override
+    public TransactionController close() throws EngineFailedException {
+        senderLink.close();
+        return this;
+    }
+
+    @Override
+    public boolean isLocallyOpen() {
+        return senderLink.isLocallyOpen();
+    }
+
+    @Override
+    public boolean isLocallyClosed() {
+        return senderLink.isLocallyClosed();
+    }
+
+    @Override
+    public TransactionController setSource(Source source) throws IllegalStateException {
+        senderLink.setSource(source);
+        return this;
+    }
+
+    @Override
+    public Source getSource() {
+        return senderLink.getSource();
+    }
+
+    @Override
+    public TransactionController setCoordinator(Coordinator coordinator) throws IllegalStateException {
+        senderLink.setTarget(coordinator);
+        return this;
+    }
+
+    @Override
+    public Coordinator getCoordinator() {
+        return senderLink.getTarget();
+    }
+
+    @Override
+    public ErrorCondition getCondition() {
+        return senderLink.getCondition();
+    }
+
+    @Override
+    public TransactionController setCondition(ErrorCondition condition) {
+        senderLink.setCondition(condition);
+        return this;
+    }
+
+    @Override
+    public Map<Symbol, Object> getProperties() {
+        return senderLink.getProperties();
+    }
+
+    @Override
+    public TransactionController setProperties(Map<Symbol, Object> properties) throws IllegalStateException {
+        senderLink.setProperties(properties);
+        return this;
+    }
+
+    @Override
+    public TransactionController setOfferedCapabilities(Symbol... offeredCapabilities) throws IllegalStateException {
+        senderLink.setOfferedCapabilities(offeredCapabilities);
+        return this;
+    }
+
+    @Override
+    public Symbol[] getOfferedCapabilities() {
+        return senderLink.getOfferedCapabilities();
+    }
+
+    @Override
+    public TransactionController setDesiredCapabilities(Symbol... desiredCapabilities) throws IllegalStateException {
+        senderLink.setDesiredCapabilities(desiredCapabilities);
+        return this;
+    }
+
+    @Override
+    public Symbol[] getDesiredCapabilities() {
+        return senderLink.getDesiredCapabilities();
+    }
+
+    @Override
+    public boolean isRemotelyOpen() {
+        return senderLink.isRemotelyOpen();
+    }
+
+    @Override
+    public boolean isRemotelyClosed() {
+        return senderLink.isRemotelyClosed();
+    }
+
+    @Override
+    public Symbol[] getRemoteOfferedCapabilities() {
+        return senderLink.getRemoteOfferedCapabilities();
+    }
+
+    @Override
+    public Symbol[] getRemoteDesiredCapabilities() {
+        return senderLink.getRemoteDesiredCapabilities();
+    }
+
+    @Override
+    public Map<Symbol, Object> getRemoteProperties() {
+        return senderLink.getRemoteProperties();
+    }
+
+    @Override
+    public ErrorCondition getRemoteCondition() {
+        return senderLink.getRemoteCondition();
+    }
+
+    @Override
+    public Source getRemoteSource() {
+        return senderLink.getRemoteSource();
+    }
+
+    @Override
+    public Coordinator getRemoteCoordinator() {
+        return senderLink.getRemoteTarget();
+    }
+
+    //----- Link event handlers
+
+    private void handleSenderLinkLocallyOpened(Sender sender) {
+        fireLocalOpen();
+    }
+
+    private void handleSenderLinkLocallyClosed(Sender sender) {
+        fireLocalClose();
+    }
+
+    private void handleSenderLinkOpened(Sender sender) {
+        fireRemoteOpen();
+    }
+
+    private void handleSenderLinkClosed(Sender sender) {
+        fireRemoteClose();
+    }
+
+    private void handleParentEndpointClosed(Sender sender) {
+        fireParentEndpointClosed();
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        fireEngineShutdown();
+    }
+
+    private void handleLinkCreditUpdated(Sender sender) {
+        if (sender.isSendable()) {
+            // Remove all that can be invoked and leave the rest in place for next credit update.
+            capacityObservers.removeIf(handler -> {
+                if (hasCapacity()) {
+                    handler.handle(this);
+                    return true;
+                }
+
+                return false;
+            });
+        }
+
+        if (sender.isDraining()) {
+            sender.drained();
+        }
+    }
+
+    private void handleDeliveryRemotelyUpdated(OutgoingDelivery delivery) {
+        ProtonControllerTransaction transaction = delivery.getLinkedResource();
+
+        DeliveryState state = delivery.getRemoteState();
+        TransactionState transactionState = transaction.getState();
+
+        try {
+            switch (state.getType()) {
+                case Declared:
+                    Declared declared = (Declared) state;
+                    transaction.setState(TransactionState.DECLARED);
+                    transaction.setTxnId(declared.getTxnId());
+                    fireDeclaredEvent(transaction);
+                    break;
+                case Accepted:
+                    transaction.setState(TransactionState.DISCHARGED);
+                    transactions.remove(transaction);
+                    fireDischargedEvent(transaction);
+                    break;
+                default:
+                    if (state.getType() == DeliveryStateType.Rejected) {
+                        Rejected rejected = (Rejected) state;
+                        transaction.setCondition(rejected.getError());
+                    }
+
+                    transactions.remove(transaction);
+
+                    if (transactionState == TransactionState.DECLARING) {
+                        transaction.setState(TransactionState.DECLARE_FAILED);
+                        fireDeclareFailureEvent(transaction);
+                    } else {
+                        transaction.setState(TransactionState.DISCHARGE_FAILED);
+                        fireDischargeFailureEvent(transaction);
+                    }
+
+                    break;
+            }
+        } finally {
+            delivery.settle();
+        }
+    }
+
+    //----- The Controller specific Transaction implementation
+
+    private final class ProtonControllerTransaction extends ProtonTransaction<TransactionController> implements Transaction<TransactionController> {
+
+        private final ProtonTransactionController controller;
+
+        public ProtonControllerTransaction(ProtonTransactionController controller) {
+            this.controller = controller;
+        }
+
+        @Override
+        public ProtonTransactionController parent() {
+            return controller;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionManager.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionManager.java
new file mode 100644
index 0000000..207957a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionManager.java
@@ -0,0 +1,453 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Transaction;
+import org.apache.qpid.protonj2.engine.Transaction.DischargeState;
+import org.apache.qpid.protonj2.engine.TransactionManager;
+import org.apache.qpid.protonj2.engine.TransactionState;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+import org.apache.qpid.protonj2.types.transactions.TransactionErrors;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+/**
+ * {@link TransactionManager} implementation that implements the abstraction
+ * around a receiver link that responds to requests to {@link Declare} and to
+ * {@link Discharge} AMQP {@link Transaction} instance.
+ */
+public final class ProtonTransactionManager extends ProtonEndpoint<TransactionManager> implements TransactionManager {
+
+    private final ProtonReceiver receiverLink;
+    private final Decoder payloadDecoder;
+
+    private EventHandler<Transaction<TransactionManager>> declareEventHandler;
+    private EventHandler<Transaction<TransactionManager>> dischargeEventHandler;
+
+    private EventHandler<TransactionManager> parentEndpointClosedEventHandler;
+
+    private Map<ProtonBuffer, ProtonManagerTransaction> transactions = new HashMap<>();
+
+    public ProtonTransactionManager(ProtonReceiver receiverLink) {
+        super(receiverLink.getEngine());
+
+        this.payloadDecoder = CodecFactory.getDecoder();
+
+        this.receiverLink = receiverLink;
+        this.receiverLink.openHandler(this::handleReceiverLinkOpened)
+                         .closeHandler(this::handleReceiverLinkClosed)
+                         .localOpenHandler(this::handleReceiverLinkLocallyOpened)
+                         .localCloseHandler(this::handleReceiverLinkLocallyClosed)
+                         .parentEndpointClosedHandler(this::handleParentEndpointClosed)
+                         .engineShutdownHandler(this::handleEngineShutdown)
+                         .deliveryReadHandler(this::handleDeliveryRead)
+                         .deliveryStateUpdatedHandler(this::handleDeliveryStateUpdate);
+    }
+
+    @Override
+    public ProtonSession getParent() {
+        return receiverLink.getSession();
+    }
+
+    @Override
+    ProtonTransactionManager self() {
+        return this;
+    }
+
+    @Override
+    public TransactionManager addCredit(int additional) {
+        receiverLink.addCredit(additional);
+        return this;
+    }
+
+    @Override
+    public int getCredit() {
+        return receiverLink.getCredit();
+    }
+
+    @Override
+    public TransactionManager declared(Transaction<TransactionManager> transaction, Binary txnId) {
+        ProtonManagerTransaction txn = (ProtonManagerTransaction) transaction;
+
+        if (txn.parent() != this) {
+            throw new IllegalArgumentException("Cannot complete declaration of a transaction from another transaction manager.");
+        }
+
+        if (txnId == null || txnId.getArray() == null || txnId.getArray().length == 0) {
+            throw new IllegalArgumentException("Cannot declare a transaction without a transaction Id");
+        }
+
+        txn.setState(TransactionState.DECLARED);
+        txn.setTxnId(txnId);
+
+        // Start tracking this transaction as active.
+        transactions.put(txnId.asProtonBuffer(), txn);
+
+        TransactionalState declaration = new TransactionalState();
+        declaration.setOutcome(Accepted.getInstance());
+        declaration.setTxnId(txnId);
+
+        txn.getDeclare().disposition(declaration, true);
+
+        return this;
+    }
+
+    @Override
+    public TransactionManager discharged(Transaction<TransactionManager> transaction) {
+        ProtonManagerTransaction txn = (ProtonManagerTransaction) transaction;
+
+        // Before sending the disposition remove if from tracking in case the write fails.
+        transactions.remove(txn.getTxnId().asProtonBuffer());
+
+        if (txn.parent() != this) {
+            throw new IllegalArgumentException("Cannot complete discharge of a transaction from another transaction manager.");
+        }
+
+        txn.setState(TransactionState.DISCHARGED);
+        txn.getDischarge().disposition(Accepted.getInstance(), true);
+
+        return this;
+    }
+
+    @Override
+    public TransactionManager declareFailed(Transaction<TransactionManager> transaction, ErrorCondition condition) {
+        ProtonManagerTransaction txn = (ProtonManagerTransaction) transaction;
+
+        if (txn.parent() != this) {
+            throw new IllegalArgumentException("Cannot fail a declared transaction from another transaction manager.");
+        }
+
+        txn.setState(TransactionState.DECLARE_FAILED);
+        txn.getDeclare().disposition(new Rejected().setError(condition), true);
+
+        return this;
+    }
+
+    @Override
+    public TransactionManager dischargeFailed(Transaction<TransactionManager> transaction, ErrorCondition condition) {
+        ProtonManagerTransaction txn = (ProtonManagerTransaction) transaction;
+
+        if (txn.parent() != this) {
+            throw new IllegalArgumentException("Cannot fail a discharge of a transaction from another transaction manager.");
+        }
+
+        transactions.remove(txn.getTxnId().asProtonBuffer());
+
+        // TODO: We should be closing the link if the remote did not report that it supports the
+        //       rejected outcome although most don't regardless of what they actually do support.
+
+        txn.setState(TransactionState.DISCHARGE_FAILED);
+        txn.getDischarge().disposition(new Rejected().setError(condition), true);
+
+        return this;
+    }
+
+    //----- Transaction event APIs
+
+    @Override
+    public TransactionManager declareHandler(EventHandler<Transaction<TransactionManager>> declaredEventHandler) {
+        this.declareEventHandler = declaredEventHandler;
+        return this;
+    }
+
+    @Override
+    public TransactionManager dischargeHandler(EventHandler<Transaction<TransactionManager>> dischargeEventHandler) {
+        this.dischargeEventHandler = dischargeEventHandler;
+        return this;
+    }
+
+    @Override
+    public TransactionManager parentEndpointClosedHandler(EventHandler<TransactionManager> handler) {
+        this.parentEndpointClosedEventHandler = handler;
+        return this;
+    }
+
+    private void fireDeclare(ProtonManagerTransaction transaction) {
+        if (declareEventHandler != null) {
+            declareEventHandler.handle(transaction);
+        }
+    }
+
+    private void fireDischarge(ProtonManagerTransaction transaction) {
+        if (dischargeEventHandler != null) {
+            dischargeEventHandler.handle(transaction);
+        }
+    }
+
+    private void fireParentEndpointClosed() {
+        if (parentEndpointClosedEventHandler != null && isLocallyOpen()) {
+            parentEndpointClosedEventHandler.handle(self());
+        }
+    }
+
+    //----- Hand off methods for link specific elements.
+
+    @Override
+    public TransactionManager open() throws IllegalStateException, EngineStateException {
+        receiverLink.open();
+        return this;
+    }
+
+    @Override
+    public TransactionManager close() throws EngineFailedException {
+        receiverLink.close();
+        return this;
+    }
+
+    @Override
+    public boolean isLocallyOpen() {
+        return receiverLink.isLocallyOpen();
+    }
+
+    @Override
+    public boolean isLocallyClosed() {
+        return receiverLink.isLocallyClosed();
+    }
+
+    @Override
+    public TransactionManager setSource(Source source) throws IllegalStateException {
+        receiverLink.setSource(source);
+        return this;
+    }
+
+    @Override
+    public Source getSource() {
+        return receiverLink.getSource();
+    }
+
+    @Override
+    public TransactionManager setCoordinator(Coordinator coordinator) throws IllegalStateException {
+        receiverLink.setTarget(coordinator);
+        return this;
+    }
+
+    @Override
+    public Coordinator getCoordinator() {
+        return receiverLink.getTarget();
+    }
+
+    @Override
+    public ErrorCondition getCondition() {
+        return receiverLink.getCondition();
+    }
+
+    @Override
+    public TransactionManager setCondition(ErrorCondition condition) {
+        receiverLink.setCondition(condition);
+        return this;
+    }
+
+    @Override
+    public Map<Symbol, Object> getProperties() {
+        return receiverLink.getProperties();
+    }
+
+    @Override
+    public TransactionManager setProperties(Map<Symbol, Object> properties) throws IllegalStateException {
+        receiverLink.setProperties(properties);
+        return this;
+    }
+
+    @Override
+    public TransactionManager setOfferedCapabilities(Symbol... offeredCapabilities) throws IllegalStateException {
+        receiverLink.setOfferedCapabilities(offeredCapabilities);
+        return this;
+    }
+
+    @Override
+    public Symbol[] getOfferedCapabilities() {
+        return receiverLink.getOfferedCapabilities();
+    }
+
+    @Override
+    public TransactionManager setDesiredCapabilities(Symbol... desiredCapabilities) throws IllegalStateException {
+        receiverLink.setDesiredCapabilities(desiredCapabilities);
+        return this;
+    }
+
+    @Override
+    public Symbol[] getDesiredCapabilities() {
+        return receiverLink.getDesiredCapabilities();
+    }
+
+    @Override
+    public boolean isRemotelyOpen() {
+        return receiverLink.isRemotelyOpen();
+    }
+
+    @Override
+    public boolean isRemotelyClosed() {
+        return receiverLink.isRemotelyClosed();
+    }
+
+    @Override
+    public Symbol[] getRemoteOfferedCapabilities() {
+        return receiverLink.getRemoteOfferedCapabilities();
+    }
+
+    @Override
+    public Symbol[] getRemoteDesiredCapabilities() {
+        return receiverLink.getRemoteDesiredCapabilities();
+    }
+
+    @Override
+    public Map<Symbol, Object> getRemoteProperties() {
+        return receiverLink.getRemoteProperties();
+    }
+
+    @Override
+    public ErrorCondition getRemoteCondition() {
+        return receiverLink.getRemoteCondition();
+    }
+
+    @Override
+    public Source getRemoteSource() {
+        return receiverLink.getRemoteSource();
+    }
+
+    @Override
+    public Coordinator getRemoteCoordinator() {
+        return receiverLink.getRemoteTarget();
+    }
+
+    //----- Link event handlers
+
+    private void handleReceiverLinkLocallyOpened(Receiver receiver) {
+        fireLocalOpen();
+    }
+
+    private void handleReceiverLinkLocallyClosed(Receiver receiver) {
+        fireLocalClose();
+    }
+
+    private void handleReceiverLinkOpened(Receiver receiver) {
+        fireRemoteOpen();
+    }
+
+    private void handleReceiverLinkClosed(Receiver receiver) {
+        fireRemoteClose();
+    }
+
+    private void handleEngineShutdown(Engine engine) {
+        fireEngineShutdown();
+    }
+
+    private void handleParentEndpointClosed(Receiver receiver) {
+        fireParentEndpointClosed();
+    }
+
+    private void handleDeliveryRead(IncomingDelivery delivery) {
+        if (delivery.isAborted()) {
+            delivery.settle();
+        } else if (!delivery.isPartial()) {
+            ProtonBuffer payload = delivery.readAll();
+
+            @SuppressWarnings( "rawtypes" )
+            AmqpValue<?> container = (AmqpValue) payloadDecoder.readObject(payload, payloadDecoder.getCachedDecoderState());
+
+            if (container.getValue() instanceof Declare) {
+                ProtonManagerTransaction transaction = new ProtonManagerTransaction(this);
+
+                transaction.setDeclare(delivery);
+                transaction.setState(TransactionState.DECLARING);
+
+                fireDeclare(transaction);
+            } else if (container.getValue() instanceof Discharge) {
+                Discharge discharge = (Discharge) container.getValue();
+                Binary txnId = discharge.getTxnId();
+
+                ProtonManagerTransaction transaction = transactions.get(txnId.asProtonBuffer());
+
+                if (transaction != null) {
+                    transaction.setState(TransactionState.DISCHARGING);
+                    transaction.setDischargeState(discharge.getFail() ? DischargeState.ROLLBACK : DischargeState.COMMIT);
+
+                    fireDischarge(transaction.setDischarge(delivery));
+                } else {
+                    // TODO: If the remote did not indicate it supports reject we should really close the link.
+                    ErrorCondition rejection = new ErrorCondition(
+                        TransactionErrors.UNKNOWN_ID, "Transaction Manager is not tracking the given transaction ID.");
+                    delivery.disposition(new Rejected(rejection), true);
+                }
+            } else {
+                throw new ProtocolViolationException("TXN Coordinator expects Declare and Dishcahrge Delivery payloads only");
+            }
+        }
+    }
+
+    private void handleDeliveryStateUpdate(IncomingDelivery delivery) {
+        // Nothing to do yet
+    }
+
+    //----- The Manager specific Transaction implementation
+
+    private static final class ProtonManagerTransaction extends ProtonTransaction<TransactionManager> {
+
+        private final ProtonTransactionManager manager;
+
+        private IncomingDelivery declare;
+        private IncomingDelivery discharge;
+
+        public ProtonManagerTransaction(ProtonTransactionManager manager) {
+            this.manager = manager;
+        }
+
+        @Override
+        public ProtonTransactionManager parent() {
+            return manager;
+        }
+
+        public ProtonManagerTransaction setDeclare(IncomingDelivery delivery) {
+            this.declare = delivery;
+            return this;
+        }
+
+        public IncomingDelivery getDeclare() {
+            return declare;
+        }
+
+        public ProtonManagerTransaction setDischarge(IncomingDelivery delivery) {
+            this.discharge = delivery;
+            return this;
+        }
+
+        public IncomingDelivery getDischarge() {
+            return discharge;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonUuidTagGenerator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonUuidTagGenerator.java
new file mode 100644
index 0000000..f9c8df2
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/ProtonUuidTagGenerator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonByteUtils;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+
+/**
+ * Built in proton {@link DeliveryTagGenerator} that creates new {@link DeliveryTag} values
+ * backed by randomly generated UUID instances.
+ */
+public class ProtonUuidTagGenerator extends ProtonDeliveryTagGenerator {
+
+    @Override
+    public DeliveryTag nextTag() {
+        return new ProtonUuidDeliveryTag(UUID.randomUUID());
+    }
+
+    private static final class ProtonUuidDeliveryTag implements DeliveryTag {
+
+        private static final int BYTES = 16;
+
+        private final UUID tagValue;
+
+        public ProtonUuidDeliveryTag(UUID tagValue) {
+            this.tagValue = tagValue;
+        }
+
+        @Override
+        public int tagLength() {
+            return BYTES;
+        }
+
+        @Override
+        public byte[] tagBytes() {
+            final byte[] tagView = new byte[BYTES];
+
+            ProtonByteUtils.writeLong(tagValue.getMostSignificantBits(), tagView, 0);
+            ProtonByteUtils.writeLong(tagValue.getLeastSignificantBits(), tagView, Long.BYTES);
+
+            return tagView;
+        }
+
+        @Override
+        public ProtonBuffer tagBuffer() {
+            return ProtonByteBufferAllocator.DEFAULT.wrap(tagBytes());
+        }
+
+        @Override
+        public void release() {
+            // Nothing to do for this tag implementation.
+        }
+
+        @Override
+        public DeliveryTag copy() {
+            return new ProtonUuidDeliveryTag(tagValue);
+        }
+
+        @Override
+        public void writeTo(ProtonBuffer buffer) {
+            buffer.writeLong(tagValue.getMostSignificantBits());
+            buffer.writeLong(tagValue.getLeastSignificantBits());
+        }
+
+        @Override
+        public int hashCode() {
+            return tagValue.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+
+            return tagValue.equals(((ProtonUuidDeliveryTag) obj).tagValue);
+        }
+
+        @Override
+        public String toString() {
+            return tagValue.toString();
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonEngineSaslDriver.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonEngineSaslDriver.java
new file mode 100644
index 0000000..b333d15
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonEngineSaslDriver.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import org.apache.qpid.protonj2.engine.EngineSaslDriver;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngine;
+import org.apache.qpid.protonj2.engine.sasl.SaslOutcome;
+
+/**
+ * Proton Engine SASL Context implementation.
+ */
+final class ProtonEngineSaslDriver implements EngineSaslDriver {
+
+    /**
+     * Default max frame size value used by this engine SASL driver if not otherwise configured.
+     */
+    public final static int DEFAULT_MAX_SASL_FRAME_SIZE = 4096;
+
+    /*
+     * The specification define lower bound for SASL frame size.
+     */
+    private final static int MIN_MAX_SASL_FRAME_SIZE = 512;
+
+    private final ProtonSaslHandler handler;
+    private final ProtonEngine engine;
+
+    private int maxFrameSize = DEFAULT_MAX_SASL_FRAME_SIZE;
+    private ProtonSaslContext context;
+
+    ProtonEngineSaslDriver(ProtonEngine engine, ProtonSaslHandler handler) {
+        this.handler = handler;
+        this.engine = engine;
+    }
+
+    @Override
+    public ProtonSaslClientContext client() {
+        if (context != null && context.isServer()) {
+            throw new IllegalStateException("Engine SASL Context already operating in server mode");
+        }
+        if (engine.state().ordinal() > EngineState.STARTED.ordinal()) {
+            throw new IllegalStateException("Engine is already shutdown or failed, cannot create client context.");
+        }
+
+        if (context == null) {
+            context = new ProtonSaslClientContext(handler);
+            // If already started we initialize here to ensure that it gets done
+            if (engine.state() == EngineState.STARTED) {
+                context.handleContextInitialization(engine);
+            }
+        }
+
+        return (ProtonSaslClientContext) context;
+    }
+
+    @Override
+    public ProtonSaslServerContext server() {
+        if (context != null && context.isClient()) {
+            throw new IllegalStateException("Engine SASL Context already operating in client mode");
+        }
+        if (engine.state().ordinal() > EngineState.STARTED.ordinal()) {
+            throw new IllegalStateException("Engine is already shutdown or failed, cannot create server context.");
+        }
+
+        if (context == null) {
+            context = new ProtonSaslServerContext(handler);
+            // If already started we initialize here to ensure that it gets done
+            if (engine.state() == EngineState.STARTED) {
+                context.handleContextInitialization(engine);
+            }
+        }
+
+        return (ProtonSaslServerContext) context;
+    }
+
+    @Override
+    public SaslState getSaslState() {
+        return context == null ? SaslState.IDLE : context.getSaslState();
+    }
+
+    @Override
+    public SaslOutcome getSaslOutcome() {
+        return context == null ? null : context.getSaslOutcome();
+    }
+
+    @Override
+    public int getMaxFrameSize() {
+        return maxFrameSize;
+    }
+
+    @Override
+    public void setMaxFrameSize(int maxFrameSize) {
+        if (getSaslState() == SaslState.IDLE) {
+            if (maxFrameSize < MIN_MAX_SASL_FRAME_SIZE) {
+                throw new IllegalArgumentException("Cannot set a max frame size lower than: " + MIN_MAX_SASL_FRAME_SIZE);
+            } else {
+                this.maxFrameSize = maxFrameSize;
+            }
+        } else {
+            throw new IllegalStateException("Cannot configure max SASL frame size after SASL negotiations have started");
+        }
+    }
+
+    //----- Internal Engine SASL Context API
+
+    void handleEngineStarting(ProtonEngine engine) {
+        if (context != null) {
+            context.handleContextInitialization(engine);
+        }
+    }
+
+    ProtonSaslContext context() {
+        return context;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientContext.java
new file mode 100644
index 0000000..3fdd413
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientContext.java
@@ -0,0 +1,348 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.security.sasl.AuthenticationException;
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngine;
+import org.apache.qpid.protonj2.engine.sasl.MechanismMismatchException;
+import org.apache.qpid.protonj2.engine.sasl.SaslClientContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslClientListener;
+import org.apache.qpid.protonj2.engine.sasl.SaslSystemException;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslChallenge;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+import org.apache.qpid.protonj2.types.security.SaslMechanisms;
+import org.apache.qpid.protonj2.types.security.SaslOutcome;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslResponse;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader.HeaderHandler;
+
+final class ProtonSaslClientContext extends ProtonSaslContext implements SaslClientContext {
+
+    private SaslClientListener client = new ProtonDefaultSaslClientListener();
+
+    // Work state trackers
+    private boolean headerWritten;
+    private boolean headerReceived;
+    private boolean mechanismsReceived;
+    private boolean mechanismChosen;
+    private boolean responseRequired;
+
+    private HeaderEnvelope pausedAMQPHeader;
+
+    public ProtonSaslClientContext(ProtonSaslHandler handler) {
+        super(handler);
+    }
+
+    @Override
+    public Role getRole() {
+        return Role.CLIENT;
+    }
+
+    @Override
+    public SaslClientContext setListener(SaslClientListener listener) {
+        Objects.requireNonNull(listener, "Cannot configure a null SaslClientListener");
+        this.client = listener;
+        return this;
+    }
+
+    @Override
+    public SaslClientListener getListener() {
+        return client;
+    }
+
+    //----- SASL negotiations API
+
+    @Override
+    public SaslClientContext sendSASLHeader() throws IllegalStateException, EngineStateException {
+        saslHandler.engine().pipeline().fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+        return this;
+    }
+
+    @Override
+    public SaslClientContext sendChosenMechanism(Symbol mechanism, String hostname, ProtonBuffer initialResponse) throws IllegalStateException, EngineStateException {
+        Objects.requireNonNull(mechanism, "Client must choose a mechanism");
+        SaslInit saslInit = new SaslInit().setHostname(hostname)
+                                          .setMechanism(mechanism)
+                                          .setInitialResponse(initialResponse);
+        saslHandler.engine().pipeline().fireWrite(new SASLEnvelope(saslInit));
+        return this;
+    }
+
+    @Override
+    public SaslClientContext sendResponse(ProtonBuffer response) throws IllegalStateException, EngineStateException {
+        Objects.requireNonNull(response);
+        saslHandler.engine().pipeline().fireWrite(new SASLEnvelope(new SaslResponse().setResponse(response)));
+        return this;
+    }
+
+    @Override
+    public SaslClientContext saslFailure(SaslException failure) {
+        if (!isDone()) {
+            done(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.SASL_PERM);
+            saslHandler.engine().engineFailed(failure);
+        }
+
+        return this;
+    }
+
+    //----- SASL Handler API sink for all reads and writes
+
+    @Override
+    ProtonSaslClientContext handleContextInitialization(ProtonEngine engine) {
+        getListener().initialize(this);
+        return this;
+    }
+
+    @Override
+    HeaderHandler<EngineHandlerContext> headerReadContext() {
+        return this.headerReadContext;
+    }
+
+    @Override
+    HeaderHandler<EngineHandlerContext> headerWriteContext() {
+        return this.headerWriteContext;
+    }
+
+    @Override
+    SaslPerformativeHandler<EngineHandlerContext> saslReadContext() {
+        return this.saslReadContext;
+    }
+
+    @Override
+    SaslPerformativeHandler<EngineHandlerContext> saslWriteContext() {
+        return this.saslWriteContext;
+    }
+
+    //----- Read and Write contexts for SASL and Header types
+
+    private final HeaderReadContext headerReadContext = new HeaderReadContext();
+    private final HeaderWriteContext headerWriteContext = new HeaderWriteContext();
+    private final SaslReadContext saslReadContext = new SaslReadContext();
+    private final SaslWriteContext saslWriteContext = new SaslWriteContext();
+
+    private final class HeaderReadContext implements HeaderHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleAMQPHeader(AMQPHeader header, EngineHandlerContext context) {
+            state = SaslState.AUTHENTICATION_FAILED;
+            context.fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+            throw new ProtocolViolationException("Remote does not support SASL authentication.");
+        }
+
+        @Override
+        public void handleSASLHeader(AMQPHeader header, EngineHandlerContext context) {
+            if (!headerReceived) {
+                headerReceived = true;
+                state = SaslState.AUTHENTICATING;
+                if (!headerWritten) {
+                    context.fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+                    headerWritten = true;
+                }
+            } else {
+                throw new ProtocolViolationException("Remote server sent illegal additional SASL headers.");
+            }
+        }
+    }
+
+    private final class HeaderWriteContext implements HeaderHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleAMQPHeader(AMQPHeader header, EngineHandlerContext context) {
+            // Hold until outcome is known, if success then forward along to start negotiation.
+            // Send a SASL header instead so that SASL negotiations can commence with the remote.
+            pausedAMQPHeader = HeaderEnvelope.AMQP_HEADER_ENVELOPE;
+            handleSASLHeader(AMQPHeader.getSASLHeader(), context);
+        }
+
+        @Override
+        public void handleSASLHeader(AMQPHeader header, EngineHandlerContext context) {
+            if (!headerWritten) {
+                headerWritten = true;
+                context.fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+            } else {
+                throw new ProtocolViolationException("SASL Header already sent to the remote SASL server");
+            }
+        }
+    }
+
+    private final class SaslReadContext implements SaslPerformativeHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleMechanisms(SaslMechanisms saslMechanisms, EngineHandlerContext context) {
+            if (!mechanismsReceived) {
+                serverMechanisms = saslMechanisms.getSaslServerMechanisms();
+                mechanismsReceived = true;
+                client.handleSaslMechanisms(ProtonSaslClientContext.this, getServerMechanisms());
+            } else {
+                throw new ProtocolViolationException("Remote sent illegal additional SASL Mechanisms frame.");
+            }
+        }
+
+        @Override
+        public void handleInit(SaslInit saslInit, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Init Frame received at SASL Client.");
+        }
+
+        @Override
+        public void handleChallenge(SaslChallenge saslChallenge, EngineHandlerContext context) {
+            if (mechanismsReceived) {
+                responseRequired = true;
+                client.handleSaslChallenge(ProtonSaslClientContext.this, saslChallenge.getChallenge());
+            } else {
+                throw new ProtocolViolationException("Remote sent unexpected SASL Challenge frame.");
+            }
+        }
+
+        @Override
+        public void handleResponse(SaslResponse saslResponse, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Response Frame received at SASL Client.");
+        }
+
+        @Override
+        public void handleOutcome(SaslOutcome saslOutcome, EngineHandlerContext context) {
+            done(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.values()[saslOutcome.getCode().ordinal()]);
+
+            SaslException saslFailure = null;
+            switch (saslOutcome.getCode()) {
+                case AUTH:
+                    saslFailure = new AuthenticationException("SASL exchange failed to authenticate client");
+                    break;
+                case OK:
+                    break;
+                case SYS:
+                    saslFailure = new SaslSystemException(true, "SASL handshake failed due to a system error");
+                    break;
+                case SYS_TEMP:
+                    saslFailure = new SaslSystemException(false, "SASL handshake failed due to a transient system error");
+                    break;
+                case SYS_PERM:
+                    saslFailure = new SaslSystemException(true, "SASL handshake failed due to a permanent system error");
+                    break;
+                default:
+                    saslFailure = new SaslException("SASL handshake failed due to an unknown error");
+                    break;
+            }
+
+            try {
+                client.handleSaslOutcome(ProtonSaslClientContext.this, getSaslOutcome(), saslOutcome.getAdditionalData());
+            } catch (Exception error) {
+                if (saslFailure == null) {
+                    saslFailure = new SaslException("Client threw unknown error while processing the outcome", error);
+                }
+            }
+
+            // Request that the SASL handler be removed from the chain now that we are done with the SASL
+            // exchange, the engine driver will remain in place holding the state for later examination.
+            context.engine().pipeline().remove(saslHandler);
+
+            if (saslFailure == null) {
+                if (pausedAMQPHeader != null) {
+                    context.fireWrite(pausedAMQPHeader);
+                }
+            } else {
+                context.engine().engineFailed(saslFailure);
+            }
+        }
+    }
+
+    private final class SaslWriteContext implements SaslPerformativeHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleMechanisms(SaslMechanisms saslMechanisms, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Mechanisms Frame written from SASL Client.");
+        }
+
+        @Override
+        public void handleInit(SaslInit saslInit, EngineHandlerContext context) {
+            if (!mechanismChosen) {
+                chosenMechanism = saslInit.getMechanism();
+                hostname = saslInit.getHostname();
+                mechanismChosen = true;
+                context.fireWrite(new SASLEnvelope(saslInit));
+            } else {
+                throw new ProtocolViolationException("SASL Init already sent to the remote SASL server");
+            }
+        }
+
+        @Override
+        public void handleChallenge(SaslChallenge saslChallenge, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Challenge Frame written from SASL Client.");
+        }
+
+        @Override
+        public void handleResponse(SaslResponse saslResponse, EngineHandlerContext context) {
+            if (responseRequired) {
+                responseRequired = false;
+                context.fireWrite(new SASLEnvelope(saslResponse));
+            } else {
+                throw new ProtocolViolationException("SASL Response is not currently expected by remote server");
+            }
+        }
+
+        @Override
+        public void handleOutcome(SaslOutcome saslOutcome, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Outcome Frame written from SASL Client.");
+        }
+    }
+
+    //----- Default SASL Client listener fails the exchange
+
+    private static class ProtonDefaultSaslClientListener implements SaslClientListener {
+
+        private final Symbol ANONYMOUS = Symbol.valueOf("ANONYMOUS");
+
+        @Override
+        public void handleSaslMechanisms(SaslClientContext context, Symbol[] mechanisms) {
+           if (mechanisms != null && Arrays.binarySearch(mechanisms, ANONYMOUS) > 0) {
+               context.sendChosenMechanism(ANONYMOUS, null, ProtonByteBufferAllocator.DEFAULT.allocate(0, 0));
+           } else {
+               ProtonSaslContext sasl = (ProtonSaslContext) context;
+               context.saslFailure(new MechanismMismatchException(
+                   "Proton default SASL handler only supports ANONYMOUS exchanges", StringUtils.toStringArray(mechanisms)));
+               sasl.done(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.SASL_SYS);
+           }
+       }
+
+        @Override
+        public void handleSaslChallenge(SaslClientContext context, ProtonBuffer challenge) {
+            ProtonSaslContext sasl = (ProtonSaslContext) context;
+            context.saslFailure(new SaslSystemException(false, "Proton default SASL handler cannot process challenge steps"));
+            sasl.done(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.SASL_SYS);
+        }
+
+        @Override
+        public void handleSaslOutcome(SaslClientContext context, org.apache.qpid.protonj2.engine.sasl.SaslOutcome outcome, ProtonBuffer additional) {
+            // Client need not do anything here the proton context handles the state updates.
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslContext.java
new file mode 100644
index 0000000..2a2bc88
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslContext.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.engine.impl.ProtonAttachments;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngine;
+import org.apache.qpid.protonj2.engine.sasl.SaslContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslOutcome;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader.HeaderHandler;
+
+/**
+ * The State engine for a SASL exchange.
+ */
+abstract class ProtonSaslContext implements SaslContext {
+
+    protected final ProtonSaslHandler saslHandler;
+
+    private ProtonAttachments attachments;
+
+    // Client negotiations tracking.
+    protected Symbol[] serverMechanisms;
+    protected Symbol chosenMechanism;
+    protected String hostname;
+    protected SaslState state = SaslState.IDLE;
+    protected SaslOutcome outcome;
+
+    private boolean done;
+
+    ProtonSaslContext(ProtonSaslHandler handler) {
+        this.saslHandler = handler;
+    }
+
+    @Override
+    public ProtonAttachments getAttachments() {
+        return attachments == null ? attachments = new ProtonAttachments() : attachments;
+    }
+
+    /**
+     * Return the Role of the context implementation.
+     *
+     * @return the Role of this SASL Context
+     */
+    @Override
+    public abstract Role getRole();
+
+    /**
+     * @return true if this is a SASL server context.
+     */
+    @Override
+    public boolean isServer() {
+        return getRole() == Role.SERVER;
+    }
+
+    /**
+     * @return true if this is a SASL client context.
+     */
+    @Override
+    public boolean isClient() {
+        return getRole() == Role.CLIENT;
+    }
+
+    @Override
+    public SaslOutcome getSaslOutcome() {
+        return outcome;
+    }
+
+    @Override
+    public SaslState getSaslState() {
+        return state;
+    }
+
+    /**
+     * @return true if SASL authentication has completed regardless of outcome
+     */
+    @Override
+    public boolean isDone() {
+        return done;
+    }
+
+    ProtonSaslContext done(SaslOutcome outcome) {
+        this.done = true;
+        this.outcome = outcome;
+        this.state = outcome == SaslOutcome.SASL_OK ? SaslState.AUTHENTICATED : SaslState.AUTHENTICATION_FAILED;
+
+        return this;
+    }
+
+    @Override
+    public Symbol getChosenMechanism() {
+        return chosenMechanism;
+    }
+
+    @Override
+    public String getHostname() {
+        return hostname;
+    }
+
+    @Override
+    public Symbol[] getServerMechanisms() {
+        return serverMechanisms != null ? Arrays.copyOf(serverMechanisms, serverMechanisms.length) : null;
+    }
+
+    ProtonSaslHandler getHandler() {
+        return saslHandler;
+    }
+
+    //----- Internal events for the specific contexts to respond to
+
+    abstract ProtonSaslContext handleContextInitialization(ProtonEngine engine);
+
+    //----- Read and Write contexts that will see all inbound and outbound activity
+
+    abstract HeaderHandler<EngineHandlerContext> headerReadContext();
+
+    abstract HeaderHandler<EngineHandlerContext> headerWriteContext();
+
+    abstract SaslPerformativeHandler<EngineHandlerContext> saslReadContext();
+
+    abstract SaslPerformativeHandler<EngineHandlerContext> saslWriteContext();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslHandler.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslHandler.java
new file mode 100644
index 0000000..eebf4ab
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslHandler.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngine;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngineNoOpSaslDriver;
+
+/**
+ * Base class used for common portions of the SASL processing pipeline.
+ */
+public final class ProtonSaslHandler implements EngineHandler {
+
+    private EngineHandlerContext context;
+    private ProtonEngineSaslDriver driver;
+    private ProtonEngine engine;
+    private ProtonSaslContext saslContext;
+
+    public boolean isDone() {
+        return saslContext != null ? saslContext.isDone() : false;
+    }
+
+    @Override
+    public void handlerAdded(EngineHandlerContext context) {
+        this.engine = (ProtonEngine) context.engine();
+        this.driver = new ProtonEngineSaslDriver(engine, this);
+        this.context = context;
+
+        engine.registerSaslDriver(driver);
+    }
+
+    @Override
+    public void handlerRemoved(EngineHandlerContext context) {
+        this.driver = null;
+        this.saslContext = null;
+        this.engine = null;
+        this.context = null;
+
+        // If the engine wasn't started then it is okay to remove this handler otherwise
+        // we would only be removed from the pipeline on completion of SASL negotiations
+        // and the driver must remain to convey the outcome.
+        if (context.engine().state() == EngineState.IDLE) {
+            ((ProtonEngine) context.engine()).registerSaslDriver(ProtonEngineNoOpSaslDriver.INSTANCE);
+        }
+    }
+
+    @Override
+    public void engineStarting(EngineHandlerContext context) {
+        driver.handleEngineStarting(engine);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+        if (isDone()) {
+            context.fireRead(header);
+        } else {
+            // Default to server if application has not configured one way or the other.
+            saslContext = driver.context();
+            if (saslContext == null) {
+                saslContext = driver.server();
+            }
+
+            header.invoke(saslContext.headerReadContext(), context);
+        }
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, SASLEnvelope frame) {
+        if (isDone()) {
+            throw new ProtocolViolationException("Unexpected SASL Frame: SASL processing has already completed");
+        } else {
+            frame.invoke(safeGetSaslContext().saslReadContext(), context);
+        }
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope frame) {
+        if (isDone()) {
+            context.fireRead(frame);
+        } else {
+            throw new ProtocolViolationException("Unexpected AMQP Frame: SASL processing not yet completed");
+        }
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, HeaderEnvelope frame) {
+        if (isDone()) {
+            context.fireWrite(frame);
+        } else {
+            // Default to client if application has not configured one way or the other.
+            saslContext = driver.context();
+            if (saslContext == null) {
+                saslContext = driver.client();
+            }
+
+            // Delegate write to the SASL Context in use to allow for state updates.
+            frame.invoke(saslContext.headerWriteContext(), context);
+        }
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope frame) {
+        if (isDone()) {
+            context.fireWrite(frame);
+        } else {
+            throw new ProtocolViolationException("Unexpected AMQP Performative: SASL processing not yet completed");
+        }
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope frame) {
+        if (isDone()) {
+            throw new ProtocolViolationException("Unexpected SASL Performative: SASL processing has yet completed");
+        } else {
+            // Delegate to the SASL Context to allow state tracking to be maintained.
+            frame.invoke(safeGetSaslContext().saslWriteContext(), context);
+        }
+    }
+
+    //----- Internal implementation API and helper methods
+
+    ProtonEngine engine() {
+        return engine;
+    }
+
+    EngineHandlerContext context() {
+        return context;
+    }
+
+    private ProtonSaslContext safeGetSaslContext() {
+        if (saslContext != null) {
+            return saslContext;
+        }
+
+        throw new IllegalStateException("Cannot process incoming SASL performative, driver not yet initialized");
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslServerContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslServerContext.java
new file mode 100644
index 0000000..57a4353
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslServerContext.java
@@ -0,0 +1,294 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngine;
+import org.apache.qpid.protonj2.engine.sasl.SaslServerContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslServerListener;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslChallenge;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+import org.apache.qpid.protonj2.types.security.SaslMechanisms;
+import org.apache.qpid.protonj2.types.security.SaslOutcome;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslResponse;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader.HeaderHandler;
+
+final class ProtonSaslServerContext extends ProtonSaslContext implements SaslServerContext {
+
+    private SaslServerListener server = new ProtonDefaultSaslServerListener();
+
+    // Work state trackers
+    private boolean headerWritten;
+    private boolean headerReceived;
+    private boolean mechanismsSent;
+    private boolean mechanismChosen;
+    private boolean responseRequired;
+
+    ProtonSaslServerContext(ProtonSaslHandler handler) {
+        super(handler);
+    }
+
+    @Override
+    public Role getRole() {
+        return Role.SERVER;
+    }
+
+    @Override
+    public SaslServerContext setListener(SaslServerListener listener) {
+        Objects.requireNonNull(listener, "Cannot configure a null SaslServerListnener");
+        this.server = listener;
+        return this;
+    }
+
+    @Override
+    public SaslServerListener getListener() {
+        return server;
+    }
+
+    //----- SASL negotiations API
+
+    @Override
+    public SaslServerContext sendMechanisms(Symbol[] mechanisms) {
+        Objects.requireNonNull(mechanisms);
+        saslHandler.engine().pipeline().fireWrite(new SASLEnvelope(new SaslMechanisms().setSaslServerMechanisms(mechanisms)));
+        return this;
+    }
+
+    @Override
+    public SaslServerContext sendChallenge(ProtonBuffer challenge) {
+        Objects.requireNonNull(challenge);
+        saslHandler.engine().pipeline().fireWrite(new SASLEnvelope(new SaslChallenge().setChallenge(challenge)));
+        return this;
+    }
+
+    @Override
+    public SaslServerContext sendOutcome(org.apache.qpid.protonj2.engine.sasl.SaslOutcome outcome, ProtonBuffer additional) {
+        Objects.requireNonNull(outcome);
+        saslHandler.engine().pipeline().fireWrite(new SASLEnvelope(new SaslOutcome().setCode(outcome.saslCode()).setAdditionalData(additional)));
+        return this;
+    }
+
+    @Override
+    public SaslServerContext saslFailure(SaslException failure) {
+        if (!isDone()) {
+            done(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.SASL_PERM);
+            saslHandler.engine().engineFailed(failure);
+        }
+        return this;
+    }
+
+    //----- SASL Handler API sink for all reads and writes
+
+    @Override
+    ProtonSaslServerContext handleContextInitialization(ProtonEngine engine) {
+        getListener().initialize(this);
+        return this;
+    }
+
+    @Override
+    HeaderHandler<EngineHandlerContext> headerReadContext() {
+        return this.headerReadContext;
+    }
+
+    @Override
+    HeaderHandler<EngineHandlerContext> headerWriteContext() {
+        return this.headerWriteContext;
+    }
+
+    @Override
+    SaslPerformativeHandler<EngineHandlerContext> saslReadContext() {
+        return this.saslReadContext;
+    }
+
+    @Override
+    SaslPerformativeHandler<EngineHandlerContext> saslWriteContext() {
+        return this.saslWriteContext;
+    }
+
+    //----- Read and Write contexts for SASL and Header types
+
+    private final HeaderReadContext headerReadContext = new HeaderReadContext();
+    private final HeaderWriteContext headerWriteContext = new HeaderWriteContext();
+    private final SaslReadContext saslReadContext = new SaslReadContext();
+    private final SaslWriteContext saslWriteContext = new SaslWriteContext();
+
+    private final class HeaderReadContext implements HeaderHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleAMQPHeader(AMQPHeader header, EngineHandlerContext context) {
+            // Raw AMQP Header shouldn't arrive before the SASL negotiations are done.
+            context.fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+            throw new ProtocolViolationException("Unexpected AMQP Header before SASL Authentication completed.");
+        }
+
+        @Override
+        public void handleSASLHeader(AMQPHeader header, EngineHandlerContext context) {
+            if (headerReceived) {
+                throw new ProtocolViolationException("Unexpected second SASL Header read before SASL Authentication completed.");
+            } else {
+                headerReceived = true;
+            }
+
+            if (!headerWritten) {
+                context.fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+                headerWritten = true;
+                state = SaslState.AUTHENTICATING;
+            }
+
+            server.handleSaslHeader(ProtonSaslServerContext.this, header);
+        }
+    }
+
+    private final class HeaderWriteContext implements HeaderHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleAMQPHeader(AMQPHeader header, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected AMQP Header write before SASL Authentication completed.");
+        }
+
+        @Override
+        public void handleSASLHeader(AMQPHeader header, EngineHandlerContext context) {
+            if (headerWritten) {
+                throw new ProtocolViolationException("Unexpected SASL write following a previous header send.");
+            }
+            headerWritten = true;
+            context.fireWrite(HeaderEnvelope.SASL_HEADER_ENVELOPE);
+        }
+    }
+
+    private final class SaslReadContext implements SaslPerformativeHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleMechanisms(SaslMechanisms saslMechanisms, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Mechanisms Frame received at SASL Server.");
+        }
+
+        @Override
+        public void handleInit(SaslInit saslInit, EngineHandlerContext context) {
+            if (mechanismChosen) {
+                throw new ProtocolViolationException("SASL Handler received second SASL Init");
+            }
+
+            hostname = saslInit.getHostname();
+            chosenMechanism = saslInit.getMechanism();
+            mechanismChosen = true;
+
+            server.handleSaslInit(ProtonSaslServerContext.this, chosenMechanism, saslInit.getInitialResponse());
+        }
+
+        @Override
+        public void handleChallenge(SaslChallenge saslChallenge, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Challenege Frame received at SASL Server.");
+        }
+
+        @Override
+        public void handleResponse(SaslResponse saslResponse, EngineHandlerContext context) {
+            if (responseRequired) {
+                server.handleSaslResponse(ProtonSaslServerContext.this, saslResponse.getResponse());
+            } else {
+                throw new ProtocolViolationException("SASL Response received when none was expected");
+            }
+        }
+
+        @Override
+        public void handleOutcome(SaslOutcome saslOutcome, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Outcome Frame received at SASL Server.");
+        }
+    }
+
+    private final class SaslWriteContext implements SaslPerformativeHandler<EngineHandlerContext> {
+
+        @Override
+        public void handleMechanisms(SaslMechanisms saslMechanisms, EngineHandlerContext context) {
+            if (!mechanismsSent) {
+                context.fireWrite(new SASLEnvelope(saslMechanisms));
+                serverMechanisms = Arrays.copyOf(saslMechanisms.getSaslServerMechanisms(), saslMechanisms.getSaslServerMechanisms().length);
+                mechanismsSent = true;
+            } else {
+                throw new ProtocolViolationException("SASL Mechanisms already sent to client");
+            }
+        }
+
+        @Override
+        public void handleInit(SaslInit saslInit, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Init Frame write attempted on SASL Server.");
+        }
+
+        @Override
+        public void handleChallenge(SaslChallenge saslChallenge, EngineHandlerContext context) {
+            if (headerWritten && mechanismsSent && !responseRequired) {
+                context.fireWrite(new SASLEnvelope(saslChallenge));
+                responseRequired = true;
+            } else {
+                throw new ProtocolViolationException("SASL Challenge sent when state does not allow it");
+            }
+        }
+
+        @Override
+        public void handleResponse(SaslResponse saslResponse, EngineHandlerContext context) {
+            throw new ProtocolViolationException("Unexpected SASL Response Frame write attempted on SASL Server.");
+        }
+
+        @Override
+        public void handleOutcome(SaslOutcome saslOutcome, EngineHandlerContext context) {
+            if (headerWritten && mechanismsSent && !responseRequired) {
+                done(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.valueOf(saslOutcome.getCode().getValue().byteValue()));
+                context.fireWrite(new SASLEnvelope(saslOutcome));
+                // Request that the SASL handler be removed from the chain now that we are done with the SASL
+                // exchange, the engine driver will remain in place holding the state for later examination.
+                context.engine().pipeline().remove(saslHandler);
+            } else {
+                throw new ProtocolViolationException("SASL Outcome sent when state does not allow it");
+            }
+        }
+    }
+
+    //----- Default SASL Server listener that fails any negotiations
+
+    public static class ProtonDefaultSaslServerListener implements SaslServerListener {
+
+        private static final Symbol[] PLAIN = { Symbol.valueOf("PLAIN") };
+
+        @Override
+        public void handleSaslHeader(SaslServerContext context, AMQPHeader header) {
+            context.sendMechanisms(PLAIN);
+        }
+
+        @Override
+        public void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse) {
+            context.sendOutcome(org.apache.qpid.protonj2.engine.sasl.SaslOutcome.SASL_AUTH, null);
+        }
+
+        @Override
+        public void handleSaslResponse(SaslServerContext context, ProtonBuffer response) {
+            throw new ProtocolViolationException("SASL Response arrived when no challenge was issued or supported.");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/MechanismMismatchException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/MechanismMismatchException.java
new file mode 100644
index 0000000..2c949ab
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/MechanismMismatchException.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import javax.security.sasl.SaslException;
+
+/**
+ * Indicates that a SASL handshake has failed because the client does not
+ * support any of the mechanisms offered by the server.
+ */
+public class MechanismMismatchException extends SaslException {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String[] offeredMechanisms;
+
+    /**
+     * Creates an exception with a detail message.
+     *
+     * @param detail
+     *        A message providing details about the cause of the problem.
+     */
+    public MechanismMismatchException(String detail) {
+        this(detail, new String[0]);
+    }
+
+    /**
+     * Creates an exception with a detail message for offered mechanisms.
+     *
+     * @param detail
+     *        A message providing details about the cause of the problem.
+     * @param mechanisms
+     *        The names of the SASL mechanisms offered by the server.
+     */
+    public MechanismMismatchException(String detail, String[] mechanisms) {
+        super(detail);
+        this.offeredMechanisms = mechanisms;
+    }
+
+    /**
+     * Gets the names of the SASL mechanisms offered by the server.
+     *
+     * @return The mechanisms.
+     */
+    public String[] getOfferedMechanisms() {
+        return offeredMechanisms;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslClientContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslClientContext.java
new file mode 100644
index 0000000..e889089
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslClientContext.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * SASL Client operating context used by an {@link Engine} that has been
+ * configured as a SASL client or that has initialed the SASL exchange by
+ * being the first to initiate the AMQP header exchange.
+ */
+public interface SaslClientContext extends SaslContext {
+
+    /**
+     * Sets the {@link SaslClientListener} that will be used to driver the client side SASL
+     * negotiations with a connected "server".  As the server initiates or responds to the
+     * various phases of the SASL negotiation the {@link SaslClientListener} will be notified
+     * and allowed to respond.
+     *
+     * @param listener
+     *      The {@link SaslClientListener} to use for SASL negotiations, cannot be null.
+     *
+     * @return this client context.
+     */
+    SaslClientContext setListener(SaslClientListener listener);
+
+    /**
+     * @return the currently set {@link SaslClientListener} instance.
+     */
+    SaslClientListener getListener();
+
+    //----- SASL Negotiation API
+
+    /**
+     * Sends the AMQP Header indicating the desire for SASL negotiations to be commenced on
+     * this connection.  The hosting application my wish to start SASL negotiations prior to
+     * opening a {@link Connection} in order to validation authentication state out of band
+     * of the normal open process.
+     *
+     * @return this client context.
+     *
+     * @throws EngineStateException if the {@link Engine} has been shut down or a failure occurs processing this header.
+     */
+    SaslClientContext sendSASLHeader() throws EngineStateException;
+
+    /**
+     * Sends a response to the SASL server indicating the chosen mechanism for this
+     * client and the host name that this client is identifying itself as.
+     *
+     * @param mechanism
+     *      The chosen mechanism selected from the list the server provided.
+     * @param host
+     *      The host name that the client is identified as or null if none selected.
+     * @param initialResponse
+     *      The initial response data sent as defined by the chosen mechanism or null if none required.
+     *
+     * @return this client context.
+     *
+     * @throws EngineStateException if the {@link Engine} has been shut down or a failure occurs processing this mechanism.
+     */
+    SaslClientContext sendChosenMechanism(Symbol mechanism, String host, ProtonBuffer initialResponse) throws EngineStateException;
+
+    /**
+     * Sends a response to a server side challenge that comprises the challenge / response
+     * exchange for the chosen SASL mechanism.
+     *
+     * @param response
+     *      The response bytes to be sent to the server for this cycle.
+     *
+     * @return this client context.
+     *
+     * @throws EngineStateException if the {@link Engine} has been shut down or a failure occurs processing this response.
+     */
+    SaslClientContext sendResponse(ProtonBuffer response) throws EngineStateException;
+
+    /**
+     * Allows the client implementation to fail the SASL negotiation process due to some
+     * unrecoverable error.  Failing the process will signal the engine that the SASL process
+     * has failed and place the engine in a failed state as well as notify the registered error
+     * handler for the {@link Engine}.
+     *
+     * @param failure
+     *      The exception to report to the {@link Engine} that describes the failure.
+     *
+     * @return this client context.
+     */
+    SaslClientContext saslFailure(SaslException failure);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslClientListener.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslClientListener.java
new file mode 100644
index 0000000..b1ba36d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslClientListener.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Listener for SASL frame arrival to facilitate relevant handling for the SASL
+ * negotiation of the client side of the SASL exchange.
+ *
+ * See the AMQP specification
+ * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-security-v1.0-os.html#doc-idp51040">
+ * SASL negotiation process</a> overview for related detail.
+ */
+public interface SaslClientListener {
+
+    /**
+     * Called to give the application code a clear point to initialize all the client side expectations.
+     * <p>
+     * The application should use this event to configure the client mechanisms and other client
+     * authentication properties.
+     * <p>
+     * In the event that the client implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslClientContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)}
+     * to signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslClientContext} used to authenticate the connection.
+     */
+    default void initialize(SaslClientContext context) {}
+
+    /**
+     * Called when a SASL mechanisms frame has arrived and its effect applied, indicating
+     * the offered mechanisms sent by the 'server' peer.  The client should respond to the
+     * mechanisms event by selecting one from the offered list and calling the
+     * {@link SaslClientContext#sendChosenMechanism(Symbol, String, ProtonBuffer)} method immediately
+     * or later using the same thread that triggered this event.
+     * <p>
+     * In the event that the client implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslClientContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} to fail
+     * the SASL negotiation and signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslClientContext} that is to handle the mechanism selection
+     * @param mechanisms
+     *      the mechanisms that the remote supports.
+     *
+     * @see SaslClientContext#sendChosenMechanism(Symbol, String, ProtonBuffer)
+     * @see SaslClientContext#saslFailure(javax.security.sasl.SaslException)
+     */
+    void handleSaslMechanisms(SaslClientContext context, Symbol[] mechanisms);
+
+    /**
+     * Called when a SASL challenge frame has arrived and its effect applied, indicating the
+     * challenge sent by the 'server' peer.  The client should respond to the mechanisms event
+     *  by selecting one from the offered list and calling the
+     * {@link SaslClientContext#sendResponse(ProtonBuffer)} method immediately or later using the same
+     * thread that triggered this event.
+     * <p>
+     * In the event that the client implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslClientContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} to fail
+     * the SASL negotiation and signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslClientContext} that is to handle the SASL challenge.
+     * @param challenge
+     *      the challenge bytes sent from the SASL server.
+     *
+     * @see SaslClientContext#sendResponse(ProtonBuffer)
+     * @see SaslClientContext#saslFailure(javax.security.sasl.SaslException)
+     */
+    void handleSaslChallenge(SaslClientContext context, ProtonBuffer challenge);
+
+    /**
+     * Called when a SASL outcome frame has arrived and its effect applied, indicating the outcome and
+     * any success additional data sent by the 'server' peer.  The client can consider the SASL negotiations
+     * complete following this event.  The client should respond appropriately to the outcome whose state can
+     * indicate that negotiations have failed and the server has not authenticated the client.
+     * <p>
+     * In the event that the client implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslClientContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} to fail
+     * the SASL negotiation and signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslClientContext} that is to handle the resulting SASL outcome.
+     * @param outcome
+     *      the outcome that was supplied by the SASL "server".
+     * @param additional
+     *      the additional data sent from the server, or null if none.
+     *
+     * @see SaslClientContext#saslFailure(javax.security.sasl.SaslException)
+     */
+    void handleSaslOutcome(SaslClientContext context, SaslOutcome outcome, ProtonBuffer additional);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslContext.java
new file mode 100644
index 0000000..94ed76e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslContext.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import org.apache.qpid.protonj2.engine.Attachments;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * The basic SASL context APIs common to both client and server sides of the SASL exchange.
+ */
+public interface SaslContext {
+
+    enum Role { CLIENT, SERVER }
+
+    /**
+     * Returns a mutable context that the application layer can use to store meaningful data for itself
+     * in relation to this specific SASL context object.
+     *
+     * @return the {@link Attachments} instance that is associated with this {@link SaslContext}
+     */
+    Attachments getAttachments();
+
+    /**
+     * Return the Role of the context implementation.
+     *
+     * @return the Role of this SASL Context
+     */
+    Role getRole();
+
+    /**
+     * @return true if SASL authentication has completed
+     */
+    boolean isDone();
+
+    /**
+     * @return true if this is a SASL server context.
+     */
+    default boolean isServer() {
+        return getRole() == Role.SERVER;
+    }
+
+    /**
+     * @return true if this is a SASL client context.
+     */
+    default boolean isClient() {
+        return getRole() == Role.SERVER;
+    }
+
+    /**
+     * Provides a low level outcome value for the SASL authentication process.
+     * <p>
+     * If the SASL exchange is ongoing or the SASL layer was skipped because a
+     * particular engine configuration allows such behavior then this method
+     * should return null to indicate no SASL outcome is available.
+     *
+     * @return the SASL outcome code that results from authentication
+     */
+    SaslOutcome getSaslOutcome();
+
+    /**
+     * Returns a SaslState that indicates the current operating state of the SASL
+     * negotiation process or conversely if no SASL layer is configured this method
+     * should return the no-SASL state.  This method must never return a null result.
+     *
+     * @return the current state of SASL Authentication.
+     */
+    SaslState getSaslState();
+
+    /**
+     * After the server has sent its supported mechanisms this method will return a
+     * copy of that list for review by the server event handler.  If called before
+     * the server has sent the mechanisms list this method will return null.
+     *
+     * @return the mechanisms that the server offered to the client.
+     */
+    Symbol[] getServerMechanisms();
+
+    /**
+     * Returns the mechanism that was sent to the server to select the SASL mechanism
+     * to use for negotiations.  If called before the client has sent its chosen mechanism
+     * this method returns null.
+     *
+     * @return the SASL mechanism that the client selected to use for negotiation.
+     */
+    Symbol getChosenMechanism();
+
+    /**
+     * The DNS name of the host (either fully qualified or relative) that was sent to the server
+     * which define the host the sending peer is connecting to.  If called before the client sent
+     * the host name information to the server this method returns null.
+     *
+     * @return the host name the client has requested to connect to.
+     */
+    String getHostname();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslOutcome.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslOutcome.java
new file mode 100644
index 0000000..c7604bc
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslOutcome.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import org.apache.qpid.protonj2.types.security.SaslCode;
+
+/**
+ * Represents the outcome of a SASL exchange
+ */
+public enum SaslOutcome {
+
+    /** authentication succeeded */
+    SASL_OK {
+
+        @Override
+        public SaslCode saslCode() {
+            return SaslCode.OK;
+        }
+    },
+    /** failed due to bad credentials */
+    SASL_AUTH {
+
+        @Override
+        public SaslCode saslCode() {
+            return SaslCode.AUTH;
+        }
+    },
+    /** failed due to a system error */
+    SASL_SYS {
+
+        @Override
+        public SaslCode saslCode() {
+            return SaslCode.SYS;
+        }
+    },
+    /** failed due to unrecoverable error */
+    SASL_PERM {
+
+        @Override
+        public SaslCode saslCode() {
+            return SaslCode.SYS_PERM;
+        }
+    },
+    /** failed due to transient error */
+    SASL_TEMP {
+
+        @Override
+        public SaslCode saslCode() {
+            return SaslCode.SYS_TEMP;
+        }
+    };
+
+    public abstract SaslCode saslCode();
+
+    /**
+     * Return a matching SASL Outcome from the given byte value.
+     *
+     * @param outcome
+     *      The byte value that is to be mapped to a SASL Outcome.
+     *
+     * @return the {@link SaslOutcome} that matches the given value.
+     *
+     * @throws IllegalArgumentException if the given outcome value is unknown.
+     */
+    public static SaslOutcome valueOf(byte outcome) {
+        switch (outcome) {
+            case 0:
+                return SASL_OK;
+            case 1:
+                return SASL_AUTH;
+            case 2:
+                return SASL_SYS;
+            case 3:
+                return SASL_PERM;
+            case 4:
+                return SASL_TEMP;
+            default:
+                throw new IllegalArgumentException("Unknown SASL outcome code provided");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslServerContext.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslServerContext.java
new file mode 100644
index 0000000..59d0dc8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslServerContext.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * SASL Server operating context used by an {@link Engine} that has been
+ * configured as a SASL server or that has receiver an AMQP header thereby
+ * forcing it into becoming the server side of the SASL exchange.
+ */
+public interface SaslServerContext extends SaslContext {
+
+    /**
+     * Sets the {@link SaslServerListener} that will be used to driver the server side SASL
+     * negotiations with a connected "client".  As the client initiates or responds to the
+     * various phases of the SASL negotiation the {@link SaslServerListener} will be notified
+     * and allowed to respond.
+     *
+     * @param listener
+     *      The {@link SaslServerListener} to use for SASL negotiations, cannot be null.
+     *
+     * @return this server context.
+     */
+    SaslServerContext setListener(SaslServerListener listener);
+
+    /**
+     * @return the currently set {@link SaslServerListener} instance.
+     */
+    SaslServerListener getListener();
+
+    //----- SASL Negotiation API
+
+    /**
+     * Sends the set of supported mechanisms to the SASL client from which it must
+     * choose and return one mechanism which will then be the basis for the SASL
+     * authentication negotiation.
+     *
+     * @param mechanisms
+     *      The mechanisms that this server supports.
+     *
+     * @return this server context.
+     *
+     * @throws EngineStateException if the engine has already shutdown or failed while processing the mechanisms.
+     */
+    SaslServerContext sendMechanisms(Symbol[] mechanisms) throws EngineStateException;
+
+    /**
+     * Sends the SASL challenge defined by the SASL mechanism that is in use during
+     * this SASL negotiation.  The challenge is an opaque binary that is provided to
+     * the server by the security mechanism.
+     *
+     * @param challenge
+     *      The buffer containing the server challenge.
+     *
+     * @return this server context.
+     *
+     * @throws EngineStateException if the engine has already shutdown or failed while sending the challenge.
+     */
+    SaslServerContext sendChallenge(ProtonBuffer challenge) throws EngineStateException;
+
+    /**
+     * Sends a response to a server side challenge that comprises the challenge / response
+     * exchange for the chosen SASL mechanism.
+     *
+     * @param outcome
+     *      The outcome of the SASL negotiation to be sent to the client.
+     * @param additional
+     *      The additional bytes to be sent from the server along with the outcome.
+     *
+     * @return this server context.
+     *
+     * @throws EngineStateException if the engine has already shutdown or failed while processing the outcome.
+     */
+    SaslServerContext sendOutcome(SaslOutcome outcome, ProtonBuffer additional) throws EngineStateException;
+
+    /**
+     * Allows the server implementation to fail the SASL negotiation process due to some
+     * unrecoverable error.  Failing the process will signal the {@link Engine} that the SASL process
+     * has failed and place the engine in a failed state as well as notify the registered error
+     * handler for the {@link Engine}.
+     *
+     * @param failure
+     *      The exception to report to the {@link Engine} that describes the failure.
+     *
+     * @return this server context.
+     */
+    SaslServerContext saslFailure(SaslException failure);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslServerListener.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslServerListener.java
new file mode 100644
index 0000000..d6ec3ce
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslServerListener.java
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+
+/**
+ * Listener for SASL frame arrival to facilitate relevant handling for the SASL
+ * negotiation of the server side of the SASL exchange.
+ *
+ * See the AMQP specification
+ * <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-security-v1.0-os.html#doc-idp51040">
+ * SASL negotiation process</a> overview for related detail.
+ */
+public interface SaslServerListener {
+
+    /**
+     * Called to give the application code a clear point to initialize all the Server side expectations.
+     * <p>
+     * The application should use this event to configure the server mechanisms and other server
+     * authentication properties.
+     * <p>
+     * In the event that the server implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslServerContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} signal
+     * the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslServerContext} used to authenticate the connection.
+     */
+    default void initialize(SaslServerContext context) {}
+
+    /**
+     * Called when the SASL header has been received and the server is now ready to send the configured SASL
+     * mechanisms.  The handler should respond be calling the {@link SaslServerContext#sendMechanisms(Symbol[])}
+     * method immediately or later using the same thread that invoked this event handler.
+     * <p>
+     * In the event that the server implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslServerContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} to fail
+     * the SASL negotiation and signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslServerContext} object that received the SASL header.
+     * @param header
+     *      the AMQP Header that was read.
+     *
+     * @see SaslServerContext#sendMechanisms(Symbol[])
+     * @see SaslServerContext#saslFailure(javax.security.sasl.SaslException)
+     */
+    void handleSaslHeader(SaslServerContext context, AMQPHeader header);
+
+    /**
+     * Called when a SASL init frame has arrived from the client indicating the chosen SASL mechanism
+     * and the initial response data if any.  Based on the chosen mechanism the server handler should provide
+     * additional challenges or complete the SASL negotiation by sending an outcome to the client.  The handler
+     * can either respond immediately or it should response using the same thread that invoked this handler.
+     * <p>
+     * In the event that the server implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslServerContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} to fail
+     * the SASL negotiation and signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslServerContext} object that is to process the SASL initial frame.
+     * @param mechanism
+     *      the SASL mechanism that the client side has chosen for negotiations.
+     * @param initResponse
+     *      the initial response sent by the remote.
+     *
+     * @see SaslServerContext#sendChallenge(ProtonBuffer)
+     * @see SaslServerContext#sendOutcome(SaslOutcome, ProtonBuffer)
+     * @see SaslServerContext#saslFailure(javax.security.sasl.SaslException)
+     */
+    void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse);
+
+    /**
+     * Called when a SASL response frame has arrived from the client.  The server should process the response
+     * and either offer additional challenges or complete the SASL negotiations based on the mechanics of the
+     * chosen SASL mechanism.  The server handler should either respond immediately or should respond from the
+     * same thread that the response handler was invoked from.
+     * <p>
+     * In the event that the server implementation cannot proceed with SASL authentication it should call the
+     * {@link SaslServerContext#saslFailure(org.apache.qpid.protonj2.engine.exceptions.SaslException)} to fail
+     * the SASL negotiation and signal the {@link Engine} that it should transition to a failed state.
+     *
+     * @param context
+     *      the {@link SaslServerContext} object that is to process the incoming response.
+     * @param response
+     *      the response sent by the remote SASL "client".
+     *
+     * @see SaslServerContext#sendChallenge(ProtonBuffer)
+     * @see SaslServerContext#sendOutcome(SaslOutcome, ProtonBuffer)
+     * @see SaslServerContext#saslFailure(javax.security.sasl.SaslException)
+     */
+    void handleSaslResponse(SaslServerContext context, ProtonBuffer response);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslSystemException.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslSystemException.java
new file mode 100644
index 0000000..df747a7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/SaslSystemException.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl;
+
+import javax.security.sasl.SaslException;
+
+/**
+ * Indicates that a SASL handshake has failed with a {@code sys},
+ * {@code sys-perm}, or {@code sys-temp} outcome code as defined by <a href=
+ * "http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-security-v1.0-os.html#type-sasl-code">
+ * AMQP Version 1.0, Section 5.3.3.6</a>.
+ */
+public class SaslSystemException extends SaslException {
+
+    private static final long serialVersionUID = 1L;
+    private final boolean permanent;
+
+    /**
+     * Creates an exception indicating a system error.
+     *
+     * @param permanent
+     *        {@code true} if the error is permanent and requires (manual)
+     *        intervention.
+     *
+     */
+    public SaslSystemException(boolean permanent) {
+        this(permanent, null);
+    }
+
+    /**
+     * Creates an exception indicating a system error with a detail message.
+     *
+     * @param permanent
+     *        <code>true</code> if the error is permanent and requires (manual) intervention.
+     * @param detail
+     *        A message providing details about the cause of the problem.
+     */
+    public SaslSystemException(boolean permanent, String detail) {
+        super(detail);
+        this.permanent = permanent;
+    }
+
+    /**
+     * Checks if the condition that caused this exception is of a permanent
+     * nature.
+     *
+     * @return {@code true} if the error condition is permanent.
+     */
+    public final boolean isPermanent() {
+        return permanent;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractMechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractMechanism.java
new file mode 100644
index 0000000..32443e0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractMechanism.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+
+/**
+ * Base class for SASL Authentication Mechanism that implements the basic
+ * methods of a Mechanism class.
+ */
+public abstract class AbstractMechanism implements Mechanism {
+
+    protected static final ProtonBuffer EMPTY = ProtonByteBufferAllocator.DEFAULT.allocate(0, 0);
+
+    @Override
+    public ProtonBuffer getInitialResponse(SaslCredentialsProvider credentials) throws SaslException {
+        return EMPTY;
+    }
+
+    @Override
+    public ProtonBuffer getChallengeResponse(SaslCredentialsProvider credentials, ProtonBuffer challenge) throws SaslException {
+        return EMPTY;
+    }
+
+    @Override
+    public void verifyCompletion() throws SaslException {
+        // Default is always valid.
+    }
+
+    @Override
+    public boolean isEnabledByDefault() {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "SASL-" + getName();
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractScramSHAMechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractScramSHAMechanism.java
new file mode 100644
index 0000000..8dc4f6e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractScramSHAMechanism.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+
+abstract class AbstractScramSHAMechanism extends AbstractMechanism {
+
+    private static final byte[] INT_1 = new byte[]{0, 0, 0, 1};
+    private static final String GS2_HEADER = "n,,";
+
+    private final String clientNonce;
+    private final String digestName;
+    private final String hmacName;
+
+    private String serverNonce;
+    private byte[] salt;
+    private int iterationCount;
+    private String clientFirstMessageBare;
+
+    private byte[] serverSignature;
+
+    private enum State {
+        INITIAL,
+        CLIENT_FIRST_SENT,
+        CLIENT_PROOF_SENT,
+        COMPLETE
+    }
+
+    private State state = State.INITIAL;
+
+    AbstractScramSHAMechanism(final String digestName, final String hmacName, final String clientNonce) {
+        this.digestName = digestName;
+        this.hmacName = hmacName;
+        this.clientNonce = clientNonce;
+    }
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        return credentials.username() != null && !credentials.username().isEmpty() &&
+               credentials.password() != null && !credentials.password().isEmpty();
+    }
+
+    @Override
+    public ProtonBuffer getInitialResponse(SaslCredentialsProvider credentials) throws SaslException {
+        if (state != State.INITIAL) {
+            throw new SaslException("Request for initial response not expected in state " + state);
+        }
+
+        StringBuilder buf = new StringBuilder("n=");
+        buf.append(escapeUsername(saslPrep(credentials.username())));
+        buf.append(",r=");
+        buf.append(clientNonce);
+        clientFirstMessageBare = buf.toString();
+        state = State.CLIENT_FIRST_SENT;
+
+        byte[] data = (GS2_HEADER + clientFirstMessageBare).getBytes(StandardCharsets.US_ASCII);
+
+        return ProtonByteBufferAllocator.DEFAULT.wrap(data).setWriteIndex(data.length);
+    }
+
+    @Override
+    public ProtonBuffer getChallengeResponse(SaslCredentialsProvider credentials, ProtonBuffer challenge) throws SaslException {
+        byte[] response;
+
+        switch (state) {
+            case CLIENT_FIRST_SENT:
+                response = calculateClientProof(credentials, challenge);
+                state = State.CLIENT_PROOF_SENT;
+                break;
+            case CLIENT_PROOF_SENT:
+                evaluateOutcome(challenge);
+                response = new byte[0];
+                state = State.COMPLETE;
+                break;
+            default:
+                throw new SaslException("No challenge expected in state " + state);
+        }
+
+        return ProtonByteBufferAllocator.DEFAULT.wrap(response).setWriteIndex(response.length);
+    }
+
+    @Override
+    public void verifyCompletion() throws SaslException {
+        super.verifyCompletion();
+
+        if (state != State.COMPLETE) {
+            throw new SaslException(String.format(
+                "SASL exchange was not fully completed.  Expected state %s but actual state %s", State.COMPLETE, state));
+        }
+    }
+
+    private byte[] calculateClientProof(SaslCredentialsProvider credentials, ProtonBuffer challenge) throws SaslException {
+        try {
+            String serverFirstMessage = challenge.toString(StandardCharsets.US_ASCII);
+            String[] parts = serverFirstMessage.split(",");
+            if (parts.length < 3) {
+                throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed");
+            } else if (parts[0].startsWith("m=")) {
+                throw new SaslException("Server requires mandatory extension which is not supported: " + parts[0]);
+            } else if (!parts[0].startsWith("r=")) {
+                throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find nonce");
+            }
+
+            String nonce = parts[0].substring(2);
+            if (!nonce.startsWith(clientNonce)) {
+                throw new SaslException("Server challenge did not use correct client nonce");
+            }
+            serverNonce = nonce;
+            if (!parts[1].startsWith("s=")) {
+                throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find salt");
+            }
+
+            String base64Salt = parts[1].substring(2);
+            salt = Base64.getDecoder().decode(base64Salt);
+            if (!parts[2].startsWith("i=")) {
+                throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find iteration count");
+            }
+
+            String iterCountString = parts[2].substring(2);
+            iterationCount = Integer.parseInt(iterCountString);
+            if (iterationCount <= 0) {
+                throw new SaslException("Iteration count " + iterationCount + " is not a positive integer");
+            }
+
+            byte[] passwordBytes = saslPrep(new String(credentials.password())).getBytes(StandardCharsets.UTF_8);
+            byte[] saltedPassword = generateSaltedPassword(passwordBytes);
+
+            String clientFinalMessageWithoutProof =
+                    "c=" + Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(StandardCharsets.US_ASCII))
+                            + ",r=" + serverNonce;
+
+            String authMessage = clientFirstMessageBare
+                    + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
+
+            byte[] clientKey = computeHmac(saltedPassword, "Client Key");
+            byte[] storedKey = MessageDigest.getInstance(digestName).digest(clientKey);
+
+            byte[] clientSignature = computeHmac(storedKey, authMessage);
+
+            byte[] clientProof = clientKey.clone();
+            for (int i = 0; i < clientProof.length; i++) {
+                clientProof[i] ^= clientSignature[i];
+            }
+
+            byte[] serverKey = computeHmac(saltedPassword, "Server Key");
+            serverSignature = computeHmac(serverKey, authMessage);
+
+            String finalMessageWithProof = clientFinalMessageWithoutProof
+                    + ",p=" + Base64.getEncoder().encodeToString(clientProof);
+
+            return finalMessageWithProof.getBytes();
+        } catch (NoSuchAlgorithmException e) {
+            throw new SaslException(e.getMessage(), e);
+        }
+    }
+
+    private void evaluateOutcome(ProtonBuffer challenge) throws SaslException {
+        String serverFinalMessage = challenge.toString(StandardCharsets.US_ASCII);
+        String[] parts = serverFinalMessage.split(",");
+
+        if (!parts[0].startsWith("v=")) {
+            throw new SaslException("Server final message did not contain verifier");
+        }
+
+        byte[] serverSignature = Base64.getDecoder().decode(parts[0].substring(2));
+
+        if (!Arrays.equals(this.serverSignature, serverSignature)) {
+            throw new SaslException("Server signature did not match");
+        }
+    }
+
+    private byte[] computeHmac(final byte[] key, final String string) throws SaslException {
+        Mac mac = createHmac(key);
+        mac.update(string.getBytes(StandardCharsets.US_ASCII));
+        return mac.doFinal();
+    }
+
+    private byte[] generateSaltedPassword(final byte[] passwordBytes) throws SaslException {
+        Mac mac = createHmac(passwordBytes);
+
+        mac.update(salt);
+        mac.update(INT_1);
+        byte[] result = mac.doFinal();
+
+        byte[] previous = null;
+        for (int i = 1; i < iterationCount; i++) {
+            mac.update(previous != null ? previous : result);
+            previous = mac.doFinal();
+            for (int x = 0; x < result.length; x++) {
+                result[x] ^= previous[x];
+            }
+        }
+
+        return result;
+    }
+
+    private Mac createHmac(final byte[] keyBytes) throws SaslException {
+        try {
+            SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
+            Mac mac = Mac.getInstance(hmacName);
+            mac.init(key);
+            return mac;
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            throw new SaslException(e.getMessage(), e);
+        }
+    }
+
+    private String saslPrep(String name) throws SaslException {
+        // TODO - a real implementation of SaslPrep [rfc4013]
+
+        if (!StandardCharsets.US_ASCII.newEncoder().canEncode(name)) {
+            throw new SaslException("Can only encode names and passwords which are restricted to ASCII characters");
+        }
+
+        return name;
+    }
+
+    private String escapeUsername(String name) {
+        name = name.replace("=", "=3D");
+        name = name.replace(",", "=2C");
+        return name;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AnonymousMechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AnonymousMechanism.java
new file mode 100644
index 0000000..406e1f8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/AnonymousMechanism.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the Anonymous SASL authentication mechanism.
+ */
+public class AnonymousMechanism extends AbstractMechanism {
+
+    public static final Symbol ANONYMOUS = Symbol.valueOf("ANONYMOUS");
+
+    @Override
+    public Symbol getName() {
+        return ANONYMOUS;
+    }
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/CramMD5Mechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/CramMD5Mechanism.java
new file mode 100644
index 0000000..6234e00
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/CramMD5Mechanism.java
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL CRAM-MD5 authentication Mechanism.
+ */
+public class CramMD5Mechanism extends AbstractMechanism {
+
+    public static final Symbol CRAM_MD5 = Symbol.valueOf("CRAM-MD5");
+
+    private static final String ASCII = "ASCII";
+    private static final String HMACMD5 = "HMACMD5";
+
+    private boolean sentResponse;
+
+    @Override
+    public Symbol getName() {
+        return CRAM_MD5;
+    }
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        return credentials.username() != null && !credentials.username().isEmpty() &&
+               credentials.password() != null && !credentials.password().isEmpty();
+    }
+
+    @Override
+    public ProtonBuffer getInitialResponse(SaslCredentialsProvider credentials) {
+        return null;
+    }
+
+    @Override
+    public ProtonBuffer getChallengeResponse(SaslCredentialsProvider credentials, ProtonBuffer challenge) throws SaslException {
+        if (!sentResponse && challenge != null && challenge.getReadableBytes() != 0) {
+            try {
+                SecretKeySpec key = new SecretKeySpec(credentials.password().getBytes(ASCII), HMACMD5);
+                Mac mac = Mac.getInstance(HMACMD5);
+                mac.init(key);
+
+                byte[] challengeBytes = new byte[challenge.getReadableBytes()];
+
+                challenge.readBytes(challengeBytes);
+
+                byte[] bytes = mac.doFinal(challengeBytes);
+
+                StringBuffer hash = new StringBuffer(credentials.username());
+                hash.append(' ');
+                for (int i = 0; i < bytes.length; i++) {
+                    String hex = Integer.toHexString(0xFF & bytes[i]);
+                    if (hex.length() == 1) {
+                        hash.append('0');
+                    }
+                    hash.append(hex);
+                }
+
+                sentResponse = true;
+
+                return ProtonByteBufferAllocator.DEFAULT.wrap(hash.toString().getBytes(ASCII));
+            } catch (UnsupportedEncodingException e) {
+                throw new SaslException("Unable to utilise required encoding", e);
+            } catch (InvalidKeyException e) {
+                throw new SaslException("Unable to utilise key", e);
+            } catch (NoSuchAlgorithmException e) {
+                throw new SaslException("Unable to utilise required algorithm", e);
+            }
+        } else {
+            return EMPTY;
+        }
+    }
+
+    @Override
+    public void verifyCompletion() throws SaslException {
+        super.verifyCompletion();
+
+        if (!sentResponse) {
+            throw new SaslException("SASL exchange was not fully completed.");
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ExternalMechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ExternalMechanism.java
new file mode 100644
index 0000000..f032059
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ExternalMechanism.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the External SASL authentication mechanism.
+ */
+public class ExternalMechanism extends AbstractMechanism {
+
+    public static final Symbol EXTERNAL = Symbol.valueOf("EXTERNAL");
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        return credentials.localPrincipal() != null;
+    }
+
+    @Override
+    public Symbol getName() {
+        return EXTERNAL;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/Mechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/Mechanism.java
new file mode 100644
index 0000000..00f0740
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/Mechanism.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Interface for all SASL authentication mechanism implementations.
+ */
+public interface Mechanism {
+
+    /**
+     * @return the well known name of this SASL mechanism.
+     */
+    Symbol getName();
+
+    /**
+     * Create an initial response based on selected mechanism.
+     *
+     * May be null if there is no initial response.
+     *
+     * @param credentials
+     *      The credentials that are supplied for this SASL negotiation.
+     *
+     * @return the initial response, or null if there isn't one.
+     *
+     * @throws SaslException if an error occurs generating the initial response.
+     */
+    ProtonBuffer getInitialResponse(SaslCredentialsProvider credentials) throws SaslException;
+
+    /**
+     * Create a response based on a given challenge from the remote peer.
+     *
+     * @param credentials
+     *      The credentials that are supplied for this SASL negotiation.
+     * @param challenge
+     *      The challenge that this Mechanism should response to.
+     *
+     * @return the response that answers the given challenge.
+     *
+     * @throws SaslException if an error occurs generating the challenge response.
+     */
+    ProtonBuffer getChallengeResponse(SaslCredentialsProvider credentials, ProtonBuffer challenge) throws SaslException;
+
+    /**
+     * Verifies that the SASL exchange has completed successfully. This is
+     * an opportunity for the mechanism to ensure that all mandatory
+     * steps have been completed successfully and to cleanup and resources
+     * that are held by this Mechanism.
+     *
+     * @throws SaslException if the outcome of the SASL exchange is not valid for this Mechanism
+     */
+    void verifyCompletion() throws SaslException;
+
+    /**
+     * Allows the Mechanism to determine if it is a valid choice based on the configured
+     * credentials at the time of selection.
+     *
+     * @param credentials
+     *      the login credentials available at the time of mechanism selection.
+     *
+     * @return true if the mechanism can be used with the provided credentials
+     */
+    boolean isApplicable(SaslCredentialsProvider credentials);
+
+    /**
+     * Allows the mechanism to indicate if it is enabled by default, or only when explicitly enabled
+     * through configuring the permitted SASL mechanisms.  Any mechanism selection logic should examine
+     * this value along with the configured allowed mechanism and decide if this one should be used.
+     *
+     * Typically most mechanisms can be enabled by default but some require explicit configuration
+     * in order to operate which implies that selecting them by default would always cause an authentication
+     * error if that mechanism matches the highest priority value offered by the remote peer.
+     *
+     * @return true if this Mechanism is enabled by default.
+     */
+    public boolean isEnabledByDefault();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/PlainMechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/PlainMechanism.java
new file mode 100644
index 0000000..363507a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/PlainMechanism.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.nio.charset.StandardCharsets;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL PLAIN authentication Mechanism.
+ *
+ * User name and Password values are sent without being encrypted.
+ */
+public class PlainMechanism extends AbstractMechanism {
+
+    public static final Symbol PLAIN = Symbol.valueOf("PLAIN");
+
+    @Override
+    public Symbol getName() {
+        return PLAIN;
+    }
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        return credentials.username() != null && !credentials.username().isEmpty() &&
+               credentials.password() != null && !credentials.password().isEmpty();
+    }
+
+    @Override
+    public ProtonBuffer getInitialResponse(SaslCredentialsProvider credentials) throws SaslException {
+        String username = credentials.username();
+        String password = credentials.password();
+
+        if (username == null) {
+            username = "";
+        }
+
+        if (password == null) {
+            password = "";
+        }
+
+        byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
+        byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
+        byte[] data = new byte[usernameBytes.length + passwordBytes.length + 2];
+
+        System.arraycopy(usernameBytes, 0, data, 1, usernameBytes.length);
+        System.arraycopy(passwordBytes, 0, data, 2 + usernameBytes.length, passwordBytes.length);
+
+        return ProtonByteBufferAllocator.DEFAULT.wrap(data).setWriteIndex(data.length);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslAuthenticator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslAuthenticator.java
new file mode 100644
index 0000000..e4bd7e1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslAuthenticator.java
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.util.Objects;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.EventHandler;
+import org.apache.qpid.protonj2.engine.sasl.SaslClientContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslClientListener;
+import org.apache.qpid.protonj2.engine.sasl.SaslOutcome;
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Handles SASL traffic from the proton engine and drives the authentication process
+ */
+public class SaslAuthenticator implements SaslClientListener {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(SaslAuthenticator.class);
+
+    private final SaslMechanismSelector selector;
+    private final SaslCredentialsProvider credentials;
+
+    private EventHandler<SaslOutcome> saslCompleteHandler;
+    private Mechanism chosenMechanism;
+
+    /**
+     * Creates a new SASL Authenticator initialized with the given credentials provider instance.  Because no
+     * {@link Mechanism} selector is given the full set of supported SASL mechanisms will be chosen from when
+     * attempting to match one to the server offered SASL mechanisms.
+     *
+     * @param credentials
+     *      The credentials that will be used when the SASL negotiation is in progress.
+     */
+    public SaslAuthenticator(SaslCredentialsProvider credentials) {
+        this(new SaslMechanismSelector(), credentials);
+    }
+
+    /**
+     * Creates a new client SASL Authenticator with the given {@link Mechanism} and client credentials
+     * provider instances.  The configured {@link Mechanism} selector is used when attempting to match
+     * a SASL {@link Mechanism} with the server offered set of supported SASL mechanisms.
+     *
+     * @param selector
+     *     The {@link SaslMechanismSelector} that will be called upon to choose a server supported mechanism.
+     * @param credentials
+     *      The credentials that will be used when the SASL negotiation is in progress.
+     */
+    public SaslAuthenticator(SaslMechanismSelector selector, SaslCredentialsProvider credentials) {
+        Objects.requireNonNull(selector, "A SASL Mechanism selector implementation is required");
+        Objects.requireNonNull(credentials, "A SASL Credentials provider implementation is required");
+
+        this.credentials = credentials;
+        this.selector = selector;
+    }
+
+    public SaslAuthenticator saslComplete(EventHandler<SaslOutcome> saslCompleteEventHandler) {
+        this.saslCompleteHandler = saslCompleteEventHandler;
+        return this;
+    }
+
+    @Override
+    public void handleSaslMechanisms(SaslClientContext context, Symbol[] mechanisms) {
+        chosenMechanism = selector.select(mechanisms, credentials);
+
+        if (chosenMechanism == null) {
+            context.saslFailure(new SaslException(
+                "Could not find a suitable SASL Mechanism. No supported mechanism, or none usable with " +
+                "the available credentials. Server offered: " + StringUtils.toStringSet(mechanisms)));
+            return;
+        }
+
+        LOG.debug("SASL Negotiations proceeding using selected mechanisms: {}", chosenMechanism);
+
+        ProtonBuffer initialResponse = null;
+        try {
+            initialResponse = chosenMechanism.getInitialResponse(credentials);
+        } catch (SaslException se) {
+            context.saslFailure(se);
+        } catch (Throwable unknown) {
+            context.saslFailure(new SaslException("Unknown error while fetching initial response", unknown));
+        }
+
+        context.sendChosenMechanism(chosenMechanism.getName(), credentials.vhost(), initialResponse);
+    }
+
+    @Override
+    public void handleSaslChallenge(SaslClientContext context, ProtonBuffer challenge) {
+        ProtonBuffer response = null;
+        try {
+            response = chosenMechanism.getChallengeResponse(credentials, challenge);
+        } catch (SaslException se) {
+            context.saslFailure(se);
+        } catch (Exception unknown) {
+            context.saslFailure(new SaslException("Unknown error while fetching challenge response", unknown));
+        }
+
+        context.sendResponse(response);
+    }
+
+    @Override
+    public void handleSaslOutcome(SaslClientContext context, SaslOutcome outcome, ProtonBuffer additional) {
+        try {
+            chosenMechanism.verifyCompletion();
+            if (saslCompleteHandler != null) {
+                saslCompleteHandler.handle(outcome);
+            }
+        } catch (SaslException se) {
+            context.saslFailure(se);
+        } catch (Exception unknown) {
+            context.saslFailure(new SaslException("Unknown error while verifying SASL negotiations completion", unknown));
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java
new file mode 100644
index 0000000..f055f6e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Interface for a supplier of login credentials used by the SASL Authenticator to
+ * select and configure the client SASL mechanism.
+ */
+public interface SaslCredentialsProvider {
+
+    String vhost();
+
+    String username();
+
+    String password();
+
+    Principal localPrincipal();
+
+    @SuppressWarnings("unchecked")
+    default Map<String, Object> options() {
+        return Collections.EMPTY_MAP;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismSelector.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismSelector.java
new file mode 100644
index 0000000..2a824d7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismSelector.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.engine.util.StringUtils;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Client side mechanism used to select a matching mechanism from the server offered list of
+ * mechanisms.  The client configures the list of allowed {@link Mechanism} names and when the
+ * server mechanisms are offered mechanism is chosen from the allowed set.  If the client does
+ * not configure any mechanisms then the selector chooses from all supported {@link Mechanism}
+ * types.
+ */
+public class SaslMechanismSelector {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(SaslMechanismSelector.class);
+
+    private final Set<Symbol> allowedMechanisms;
+
+    /**
+     * Creates a new {@link Mechanism} selector that will choose a match from all supported {@link Mechanism} types.
+     */
+    public SaslMechanismSelector() {
+        this((Set<Symbol>) null);
+    }
+
+    /**
+     * Creates a new {@link Mechanism} selector configured with the given set of allowed {@link Mechanism} names.
+     *
+     * @param allowed
+     *      A {@link Collection} of SASL mechanism names that are allowed to be used when selecting a matching mechanism.
+     */
+    @SuppressWarnings("unchecked")
+    public SaslMechanismSelector(Collection<String> allowed) {
+        this.allowedMechanisms = allowed != null ? StringUtils.toSymbolSet(allowed) : Collections.EMPTY_SET;
+    }
+
+    /**
+     * Creates a new {@link Mechanism} selector configured with the given set of allowed {@link Mechanism} names.
+     *
+     * @param allowed
+     *      A {@link Set} of SASL mechanism names that are allowed to be used when selecting a matching mechanism.
+     */
+    @SuppressWarnings("unchecked")
+    public SaslMechanismSelector(Set<Symbol> allowed) {
+        this.allowedMechanisms = allowed != null ? allowed : Collections.EMPTY_SET;
+    }
+
+    /**
+     * @return the configured set of allowed SASL {@link Mechanism} names.
+     */
+    public Set<Symbol> getAllowedMechanisms() {
+        return Collections.unmodifiableSet(allowedMechanisms);
+    }
+
+    /**
+     * Given a list of SASL mechanism names select a match from the supported types using the
+     * configured allowed list and the given credentials.
+     *
+     * @param serverMechs
+     *      The list of mechanisms the server indicates it supports.
+     * @param credentials
+     *      A {@link SaslCredentialsProvider} used to choose an matching applicable SASL {@link Mechanism}.
+     *
+     * @return a selected SASL {@link Mechanism} instance or null of no match is possible.
+     */
+    public Mechanism select(Symbol[] serverMechs, SaslCredentialsProvider credentials) {
+        Set<Symbol> candidates = new LinkedHashSet<>(serverMechs.length);
+        for (Symbol serverMech : serverMechs) {
+            candidates.add(serverMech);
+        }
+
+        if (!allowedMechanisms.isEmpty()) {
+            candidates.retainAll(allowedMechanisms);
+        }
+
+        for (Symbol match : candidates) {
+            LOG.trace("Attempting to match offered mechanism {} with supported and configured mechanisms", match);
+
+            try {
+                final Mechanism mechanism = createMechanism(match, credentials);
+                if (mechanism == null) {
+                    LOG.debug("Skipping {} mechanism as no implementation could be created to support it", match);
+                    continue;
+                }
+
+                if (!isApplicable(mechanism, credentials)) {
+                    LOG.trace("Skipping {} mechanism as it is not applicable", mechanism);
+                } else {
+                    return mechanism;
+                }
+            } catch (Exception error) {
+                LOG.warn("Caught exception while trying to create SASL mechanism {}: {}", match, error.getMessage());
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Using the given {@link Mechanism} name and the provided credentials create and configure a
+     * {@link Mechanism} for evaluation by the selector.
+     *
+     * @param name
+     *      A mechanism name that matches one of the supported offerings by the remote
+     * @param credentials
+     *      The provided credentials that will be used to perform authentication with the remote.
+     *
+     * @return a new {@link Mechanism} instance or null the offered mechanism is unsupported.
+     */
+    protected Mechanism createMechanism(Symbol name, SaslCredentialsProvider credentials) {
+        return SaslMechanisms.valueOf(name).createMechanism();
+    }
+
+    /**
+     * Tests a given {@link Mechanism} instance to determine if it is applicable given the selector
+     * configuration and the provided credentials.
+     *
+     * @param candidate
+     *      The SASL mechanism that matches both the allowed and the server offered lists.
+     * @param credentials
+     *      The provided SASL credentials which will be used when authenticating with the remote.
+     *
+     * @return true if the candidate {@link Mechanism} is applicable given the provide credentials.
+     */
+    protected boolean isApplicable(Mechanism candidate, SaslCredentialsProvider credentials) {
+        Objects.requireNonNull(candidate, "Candidate Mechanism to validate must not be null");
+
+        // If a match is found we still may skip it if the credentials given do not match with
+        // what is needed in order for it to operate.  We also need to check that when working
+        // from a wide open mechanism selection range we check is the Mechanism supports use in
+        // a default configuration with no pre-configuration.
+
+        if (!candidate.isApplicable(credentials)) {
+            LOG.debug("Skipping {} mechanism because the available credentials are not sufficient", candidate.getName());
+            return false;
+        }
+
+        if (allowedMechanisms.isEmpty()) {
+            return candidate.isEnabledByDefault();
+        } else {
+            return allowedMechanisms.contains(candidate.getName());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java
new file mode 100644
index 0000000..8a3442c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java
@@ -0,0 +1,178 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Enumeration of all SASL Mechanisms supported by the client, order should be from most secure
+ * to least secure.
+ */
+public enum SaslMechanisms {
+
+    EXTERNAL {
+
+        private final Mechanism INSTANCE = new ExternalMechanism();
+
+        @Override
+        public Symbol getName() {
+            return ExternalMechanism.EXTERNAL;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return INSTANCE;
+        }
+    },
+    SCRAM_SHA_512 {
+
+        @Override
+        public Symbol getName() {
+            return ScramSHA512Mechanism.SCRAM_SHA_512;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return new ScramSHA512Mechanism();
+        }
+    },
+    SCRAM_SHA_256 {
+
+        @Override
+        public Symbol getName() {
+            return ScramSHA256Mechanism.SCRAM_SHA_256;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return new ScramSHA256Mechanism();
+        }
+    },
+    SCRAM_SHA_1 {
+
+        @Override
+        public Symbol getName() {
+            return ScramSHA1Mechanism.SCRAM_SHA_1;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return new ScramSHA1Mechanism();
+        }
+    },
+    CRAM_MD5 {
+
+        @Override
+        public Symbol getName() {
+            return CramMD5Mechanism.CRAM_MD5;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return new CramMD5Mechanism();
+        }
+    },
+    PLAIN {
+
+        private final Mechanism INSTANCE = new PlainMechanism();
+
+        @Override
+        public Symbol getName() {
+            return PlainMechanism.PLAIN;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return INSTANCE;
+        }
+    },
+    XOAUTH2 {
+
+        @Override
+        public Symbol getName() {
+            return XOauth2Mechanism.XOAUTH2;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return new XOauth2Mechanism();
+        }
+    },
+    ANONYMOUS {
+
+        private final Mechanism INSTANCE = new AnonymousMechanism();
+
+        @Override
+        public Symbol getName() {
+            return AnonymousMechanism.ANONYMOUS;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return INSTANCE;
+        }
+    };
+
+    /**
+     * @return the {@link Symbol} that represents the {@link Mechanism} name.
+     */
+    public abstract Symbol getName();
+
+    /**
+     * Creates the object that implements the SASL Mechanism represented by this enumeration.
+     *
+     * @return a new SASL {@link Mechanism} type that will be used for authentication.
+     */
+    public abstract Mechanism createMechanism();
+
+    /**
+     * Returns the matching {@link SaslMechanisms} enumeration value for the given
+     * {@link Symbol} key.
+     *
+     * @param mechanism
+     * 		The symbolic mechanism name to lookup.
+     *
+     * @return the matching {@link SaslMechanisms} for the given Symbol value.
+     */
+    public static SaslMechanisms valueOf(Symbol mechanism) {
+        for (SaslMechanisms value : SaslMechanisms.values()) {
+            if (value.getName().equals(mechanism)) {
+                return value;
+            }
+        }
+
+        throw new IllegalArgumentException("No Macthing SASL Mechanism with name: " + mechanism.toString());
+    }
+
+    /**
+     * Given a mechanism name, validate that it is one of the mechanisms this client supports.
+     *
+     * @param mechanism
+     * 		The mechanism name to validate
+     *
+     * @return true if the name matches a supported SASL Mechanism
+     */
+    public static boolean validate(String mechanism) {
+        for (SaslMechanisms supported : SaslMechanisms.values()) {
+            if (supported.getName().toString().equals(mechanism.toUpperCase())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA1Mechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA1Mechanism.java
new file mode 100644
index 0000000..d291277
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA1Mechanism.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL Scram SHA1 authentication Mechanism.
+ */
+public class ScramSHA1Mechanism extends AbstractScramSHAMechanism {
+
+    public static final String SHA_1 = "SHA-1";
+    public static final String HMAC_SHA_1 = "HmacSHA1";
+
+    public static final Symbol SCRAM_SHA_1 = Symbol.valueOf("SCRAM-SHA-1");
+
+    public ScramSHA1Mechanism() {
+        this(UUID.randomUUID().toString());
+    }
+
+    /** For unit testing */
+    ScramSHA1Mechanism(String clientNonce) {
+        super(SHA_1, HMAC_SHA_1, clientNonce);
+    }
+
+    @Override
+    public Symbol getName() {
+        return SCRAM_SHA_1;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA256Mechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA256Mechanism.java
new file mode 100644
index 0000000..946f6be
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA256Mechanism.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL Scram SHA 256 authentication Mechanism.
+ */
+public class ScramSHA256Mechanism extends AbstractScramSHAMechanism {
+
+    public static final String SHA_256 = "SHA-256";
+    public static final String HMAC_SHA_256 = "HmacSHA256";
+
+    public static final Symbol SCRAM_SHA_256 = Symbol.valueOf("SCRAM-SHA-256");
+
+    public ScramSHA256Mechanism() {
+        this(UUID.randomUUID().toString());
+    }
+
+    /** For unit testing */
+    ScramSHA256Mechanism(String clientNonce) {
+        super(SHA_256, HMAC_SHA_256, clientNonce);
+    }
+
+    @Override
+    public Symbol getName() {
+        return SCRAM_SHA_256;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA512Mechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA512Mechanism.java
new file mode 100644
index 0000000..9a38a61
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA512Mechanism.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL Scram SHA 256 authentication Mechanism.
+ */
+public class ScramSHA512Mechanism extends AbstractScramSHAMechanism {
+
+    public static final String SHA_512 = "SHA-512";
+    public static final String HMAC_SHA_512 = "HmacSHA512";
+
+    public static final Symbol SCRAM_SHA_512 = Symbol.valueOf("SCRAM-SHA-512");
+
+    public ScramSHA512Mechanism() {
+        this(UUID.randomUUID().toString());
+    }
+
+    /** For unit testing */
+    ScramSHA512Mechanism(String clientNonce) {
+        super(SHA_512, HMAC_SHA_512, clientNonce);
+    }
+
+    @Override
+    public Symbol getName() {
+        return SCRAM_SHA_512;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/XOauth2Mechanism.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/XOauth2Mechanism.java
new file mode 100644
index 0000000..627cdb7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/XOauth2Mechanism.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL XOAUTH2 authentication Mechanism .
+ *
+ * User name and Password values are sent without being encrypted.
+ */
+public class XOauth2Mechanism extends AbstractMechanism {
+
+    private final Pattern ACCESS_TOKEN_PATTERN = Pattern.compile("^[\\x20-\\x7F]+$");
+
+    public static final Symbol XOAUTH2 = Symbol.valueOf("XOAUTH2");
+
+    private String additionalFailureInformation;
+
+    @Override
+    public Symbol getName() {
+        return XOAUTH2;
+    }
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        if (credentials.username() != null && !credentials.username().isEmpty()  &&
+            credentials.password() != null && !credentials.password().isEmpty()) {
+
+            return ACCESS_TOKEN_PATTERN.matcher(credentials.password()).matches();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public ProtonBuffer getInitialResponse(SaslCredentialsProvider credentials) throws SaslException {
+
+        String username = credentials.username();
+        String password = credentials.password();
+
+        if (username == null) {
+            username = "";
+        }
+
+        if (password == null) {
+            password = "";
+        }
+
+        byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
+        byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
+        byte[] data = new byte[usernameBytes.length + passwordBytes.length + 20];
+        System.arraycopy("user=".getBytes(StandardCharsets.US_ASCII), 0, data, 0, 5);
+        System.arraycopy(usernameBytes, 0, data, 5, usernameBytes.length);
+        data[5+usernameBytes.length] = 1;
+        System.arraycopy("auth=Bearer ".getBytes(StandardCharsets.US_ASCII), 0, data, 6+usernameBytes.length, 12);
+        System.arraycopy(passwordBytes, 0, data, 18 + usernameBytes.length, passwordBytes.length);
+        data[data.length-2] = 1;
+        data[data.length-1] = 1;
+
+        return ProtonByteBufferAllocator.DEFAULT.wrap(data).setWriteIndex(data.length);
+    }
+
+    @Override
+    public ProtonBuffer getChallengeResponse(SaslCredentialsProvider credentials, ProtonBuffer challenge) throws SaslException {
+        if (challenge != null && challenge.getReadableBytes() > 0 && additionalFailureInformation == null) {
+            additionalFailureInformation = challenge.toString(StandardCharsets.UTF_8);
+        }
+
+        return EMPTY;
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/DeliveryIdTracker.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/DeliveryIdTracker.java
new file mode 100644
index 0000000..206e01a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/DeliveryIdTracker.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Tracker of Delivery ID values, implements a sequence number and provides ability to
+ * keep an not set state for use when allowing for set / not set tracking.
+ */
+public class DeliveryIdTracker extends Number implements Comparable<DeliveryIdTracker>  {
+
+    private static final long serialVersionUID = -334270502498254343L;
+
+    private int deliveryId;
+    private boolean empty = true;
+
+    /**
+     * Create a new Delivery Id tracker with initial state.
+     */
+    public DeliveryIdTracker() {}
+
+    /**
+     * Create a new Delivery Id tracker with initial state.
+     *
+     * @param startValue
+     *      The initial value to assign this tracker.
+     */
+    public DeliveryIdTracker(int startValue) {
+        deliveryId = startValue;
+        empty = false;
+    }
+
+    /**
+     * Sets the current delivery ID value for this {@link DeliveryIdTracker}
+     *
+     * @param value
+     *      The new value to assign as the delivery ID.
+     */
+    public void set(int value) {
+        deliveryId = value;
+        empty = false;
+    }
+
+    /**
+     * Clears the tracked value and marks this tracker as empty.
+     */
+    public void reset() {
+        deliveryId = 0;
+        empty = true;
+    }
+
+    public boolean isEmpty() {
+        return empty;
+    }
+
+    public int compareTo(Number other) {
+        if (isEmpty()) {
+            return -1;
+        } else {
+            return Integer.compareUnsigned(intValue(), other.intValue());
+        }
+    }
+
+    public int compareTo(int other) {
+        if (isEmpty()) {
+            return -1;
+        } else {
+            return Integer.compareUnsigned(intValue(), other);
+        }
+    }
+
+    @Override
+    public int compareTo(DeliveryIdTracker other) {
+        if (isEmpty() || other.isEmpty()) {
+            return -1;
+        } else {
+            return Integer.compareUnsigned(intValue(), other.deliveryId);
+        }
+    }
+
+    @Override
+    public int intValue() {
+        return deliveryId;
+    }
+
+    @Override
+    public long longValue() {
+        return Integer.toUnsignedLong(deliveryId);
+    }
+
+    @Override
+    public float floatValue() {
+        return Float.intBitsToFloat(deliveryId);
+    }
+
+    @Override
+    public double doubleValue() {
+        return Float.floatToIntBits(deliveryId);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof DeliveryIdTracker) {
+            return ((DeliveryIdTracker) other).deliveryId == this.deliveryId;
+        } else if (other instanceof SequenceNumber) {
+            return ((SequenceNumber) other).intValue() == this.deliveryId;
+        } else if (other instanceof UnsignedInteger) {
+            return ((UnsignedInteger) other).intValue() == this.deliveryId;
+        }
+
+        return false;
+    }
+
+    public boolean equals(int other) {
+        return Integer.compareUnsigned(deliveryId, other) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Integer.hashCode(deliveryId);
+    }
+
+    public UnsignedInteger toUnsignedInteger() {
+        return empty ? null : UnsignedInteger.valueOf(deliveryId);
+    }
+
+    @Override
+    public String toString() {
+        return Integer.toUnsignedString(deliveryId);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/LinkedSplayMap.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/LinkedSplayMap.java
new file mode 100644
index 0000000..3aa545b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/LinkedSplayMap.java
@@ -0,0 +1,408 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * @param <E> The type stored in the map entries
+ */
+public class LinkedSplayMap<E> extends SplayMap<E> {
+
+    /**
+     * A dummy entry in the circular linked list of entries in the map.
+     * The first real entry is root.next, and the last is header.pervious.
+     * If the map is empty, root.next == root && root.previous == root.
+     */
+    private final transient SplayedEntry<E> entries = new SplayedEntry<>();
+
+    @Override
+    public void clear() {
+        super.clear();
+
+        // Unlink all the entries and reset to no insertions state
+        entries.linkNext = entries.linkPrev = entries;
+    }
+
+    @Override
+    public Set<UnsignedInteger> keySet() {
+        if (keySet == null) {
+            keySet = new LinkedSplayMapKeySet();
+        }
+        return keySet;
+    }
+
+    @Override
+    public Collection<E> values() {
+        if (values == null) {
+            values = new LinkedSplayMapValues();
+        }
+        return values;
+    }
+
+    @Override
+    public Set<Entry<UnsignedInteger, E>> entrySet() {
+        if (entrySet == null) {
+            entrySet = new LinkedSplayMapEntrySet();
+        }
+        return entrySet;
+    }
+
+    @Override
+    public void forEach(BiConsumer<? super UnsignedInteger, ? super E> action) {
+        Objects.requireNonNull(action);
+
+        SplayedEntry<E> root = this.entries;
+        for (SplayedEntry<E> entry = root.linkNext; entry != root; entry = entry.linkNext) {
+            action.accept(entry.getKey(), entry.getValue());
+        }
+    }
+
+    @Override
+    public void forEach(Consumer<? super E> action) {
+        Objects.requireNonNull(action);
+
+        SplayedEntry<E> root = this.entries;
+        for (SplayedEntry<E> entry = root.linkNext; entry != root; entry = entry.linkNext) {
+            action.accept(entry.getValue());
+        }
+    }
+
+    @Override
+    public void replaceAll(BiFunction<? super UnsignedInteger, ? super E, ? extends E> function) {
+        Objects.requireNonNull(function, "The replacement function parameter cannot be null");
+
+        final int initialModCount = modCount;
+        for (SplayedEntry<E> e = entries.linkNext; e != entries; e = e.linkNext) {
+            e.value = function.apply(e.getKey(), e.value);
+        }
+
+        if (modCount != initialModCount) {
+            throw new ConcurrentModificationException();
+        }
+    }
+
+    @Override
+    protected void entryAdded(SplayedEntry<E> newEntry) {
+        // Insertion ordering of the splayed map entries recorded here
+        // and the list of entries doesn't change until an entry is removed.
+        newEntry.linkNext = entries;
+        newEntry.linkPrev = entries.linkPrev;
+        entries.linkPrev.linkNext = newEntry;
+        entries.linkPrev = newEntry;
+    }
+
+    @Override
+    protected void entryDeleted(SplayedEntry<E> deletedEntry) {
+        // Remove the entry from the insertion ordered entry list.
+        deletedEntry.linkNext.linkPrev = deletedEntry.linkPrev;
+        deletedEntry.linkPrev.linkNext = deletedEntry.linkNext;
+        deletedEntry.linkNext = deletedEntry.linkPrev = null;
+    }
+
+    //----- Map Iterator implementation for EntrySet, KeySet and Values collections
+
+    // Base class iterator that can be used for the collections returned from the Map
+    private abstract class LinkedSplayMapIterator<T> implements Iterator<T> {
+
+        private SplayedEntry<E> nextNode;
+        private SplayedEntry<E> lastReturned;
+
+        private int expectedModCount;
+
+        public LinkedSplayMapIterator(SplayedEntry<E> startAt) {
+            this.nextNode = startAt;
+            this.expectedModCount = LinkedSplayMap.this.modCount;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return nextNode != entries;
+        }
+
+        protected SplayedEntry<E> nextNode() {
+            final SplayedEntry<E> entry = nextNode;
+
+            if (nextNode == entries) {
+                throw new NoSuchElementException();
+            }
+            if (expectedModCount != LinkedSplayMap.this.modCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            nextNode = entry.linkNext;
+
+            return lastReturned = entry;
+        }
+
+        // Unused as of now but can be used for NavigableMap amongst other things
+        @SuppressWarnings("unused")
+        protected SplayedEntry<E> previousNode() {
+            final SplayedEntry<E> entry = nextNode;
+
+            if (nextNode == entries) {
+                throw new NoSuchElementException();
+            }
+            if (expectedModCount != LinkedSplayMap.this.modCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            nextNode = nextNode.linkPrev;
+
+            return lastReturned = entry;
+        }
+
+        @Override
+        public void remove() {
+            if (lastReturned == null) {
+                throw new IllegalStateException();
+            }
+            if (modCount != expectedModCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            delete(lastReturned);
+
+            expectedModCount = modCount;
+            lastReturned = null;
+        }
+    }
+
+    private class LinkedSplayMapEntryIterator extends LinkedSplayMapIterator<Entry<UnsignedInteger, E>> {
+
+        public LinkedSplayMapEntryIterator(SplayedEntry<E> startAt) {
+            super(startAt);
+        }
+
+        @Override
+        public Entry<UnsignedInteger, E> next() {
+            return nextNode();
+        }
+    }
+
+    private class LinkedSplayMapKeyIterator extends LinkedSplayMapIterator<UnsignedInteger> {
+
+        public LinkedSplayMapKeyIterator(SplayedEntry<E> startAt) {
+            super(startAt);
+        }
+
+        @Override
+        public UnsignedInteger next() {
+            return nextNode().getKey();
+        }
+    }
+
+    private class LinkedSplayMapValueIterator extends LinkedSplayMapIterator<E> {
+
+        public LinkedSplayMapValueIterator(SplayedEntry<E> startAt) {
+            super(startAt);
+        }
+
+        @Override
+        public E next() {
+            return nextNode().getValue();
+        }
+    }
+
+    //----- Splay Map Collection types
+
+    private final class LinkedSplayMapValues extends AbstractCollection<E> {
+
+        @Override
+        public Iterator<E> iterator() {
+            return new LinkedSplayMapValueIterator(entries.linkNext);
+        }
+
+        @Override
+        public int size() {
+            return LinkedSplayMap.this.size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return LinkedSplayMap.this.containsValue(o);
+        }
+
+        @Override
+        public boolean remove(Object target) {
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+
+            for (SplayedEntry<E> entry = root.linkNext; entry != root; entry = entry.linkNext) {
+                if (entry.valueEquals(target)) {
+                    delete(entry);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void forEach(Consumer<? super E> action) {
+            Objects.requireNonNull(action, "forEach action paremeter cannot be null");
+
+            final int initialModCount = modCount;
+
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+            for (SplayedEntry<E> entry = root.linkNext; entry != entries; entry = entry.linkNext) {
+                action.accept(entry.value);
+            }
+
+            if (modCount != initialModCount) {
+                throw new ConcurrentModificationException();
+            }
+        }
+
+        @Override
+        public void clear() {
+            LinkedSplayMap.this.clear();
+        }
+    }
+
+    private final class LinkedSplayMapKeySet extends AbstractSet<UnsignedInteger> {
+
+        @Override
+        public Iterator<UnsignedInteger> iterator() {
+            return new LinkedSplayMapKeyIterator(entries.linkNext);
+        }
+
+        @Override
+        public int size() {
+            return LinkedSplayMap.this.size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return LinkedSplayMap.this.containsKey(o);
+        }
+
+        @Override
+        public boolean remove(Object target) {
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+
+            for (SplayedEntry<E> entry = root.linkNext; entry != root; entry = entry.linkNext) {
+                if (entry.keyEquals(target)) {
+                    delete(entry);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void forEach(Consumer<? super UnsignedInteger> action) {
+            Objects.requireNonNull(action, "forEach action paremeter cannot be null");
+
+            final int initialModCount = modCount;
+
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+            for (SplayedEntry<E> entry = root.linkNext; entry != entries; entry = entry.linkNext) {
+                action.accept(entry.getKey());
+            }
+
+            if (modCount != initialModCount) {
+                throw new ConcurrentModificationException();
+            }
+        }
+
+        @Override
+        public void clear() {
+            LinkedSplayMap.this.clear();
+        }
+    }
+
+    private final class LinkedSplayMapEntrySet extends AbstractSet<Entry<UnsignedInteger, E>> {
+
+        @Override
+        public Iterator<Entry<UnsignedInteger, E>> iterator() {
+            return new LinkedSplayMapEntryIterator(entries.linkNext);
+        }
+
+        @Override
+        public int size() {
+            return LinkedSplayMap.this.size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            if (!(o instanceof Map.Entry)) {
+                return false;
+            }
+
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+
+            for (SplayedEntry<E> entry = root.linkNext; entry != root; entry = entry.linkNext) {
+                if (entry.equals(o)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public void forEach(Consumer<? super Entry<UnsignedInteger, E>> action) {
+            Objects.requireNonNull(action, "forEach action paremeter cannot be null");
+
+            final int initialModCount = modCount;
+
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+
+            for (SplayedEntry<E> entry = root.linkNext; entry != entries; entry = entry.linkNext) {
+                action.accept(entry);
+            }
+
+            if (modCount != initialModCount) {
+                throw new ConcurrentModificationException();
+            }
+        }
+
+        @Override
+        public boolean remove(Object target) {
+            if (!(target instanceof Entry)) {
+                throw new IllegalArgumentException("value provided is not an Entry type.");
+            }
+
+            final SplayedEntry<E> root = LinkedSplayMap.this.entries;
+
+            for (SplayedEntry<E> entry = root.linkNext; entry != root; entry = entry.linkNext) {
+                if (entry.equals(target)) {
+                    delete(entry);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void clear() {
+            LinkedSplayMap.this.clear();
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/RingQueue.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/RingQueue.java
new file mode 100644
index 0000000..7dfc723
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/RingQueue.java
@@ -0,0 +1,240 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.AbstractQueue;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.function.Supplier;
+
+/**
+ * Simple Ring Queue implementation that has an enforced max size value.
+ *
+ * @param <E> the element type that is stored in this {@link Queue} type.
+ */
+public class RingQueue<E> extends AbstractQueue<E> {
+
+    private int read = 0;
+    private int write = -1;
+    private int size;
+
+    private final Object[] backingArray;
+
+    public RingQueue(int queueSize) {
+        this.backingArray = new Object[queueSize];
+    }
+
+    public RingQueue(Collection<E> collection) {
+        this.backingArray = new Object[collection.size()];
+
+        collection.forEach(value -> offer(value));
+    }
+
+    @Override
+    public boolean offer(E e) {
+        if (isFull()) {
+            return false;
+        } else {
+            write = advance(write, backingArray.length);
+            size++;
+            backingArray[write] = e;
+            return true;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public E poll() {
+        final E result;
+
+        if (isEmpty()) {
+            result = null;
+        } else {
+            result = (E) backingArray[read];
+            backingArray[read] = null;
+            read = advance(read, backingArray.length);
+            size--;
+        }
+
+        return result;
+    }
+
+    public E poll(Supplier<E> createOnEmpty) {
+        if (isEmpty()) {
+            return createOnEmpty.get();
+        } else {
+            return poll();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public E peek() {
+        return isEmpty() ? null : (E) backingArray[read];
+    }
+
+    @Override
+    public boolean addAll(Collection<? extends E> c) {
+        Objects.requireNonNull(c, "Given collection to add was null");
+
+        if (c == this) {
+            throw new IllegalArgumentException("Cannot add a Queue to itself");
+        }
+        if (c.size() > backingArray.length - size) {
+            throw new IllegalStateException("Insuficcient sapce to add all elements of the collection to the queue");
+        }
+
+        c.forEach(value -> offer(value));
+
+        return !c.isEmpty();
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        throw new UnsupportedOperationException("Cannot remove other than from the Queue head methods");
+    }
+
+    @Override
+    public boolean removeAll(Collection<?> c) {
+        Objects.requireNonNull(c);
+        throw new UnsupportedOperationException("Cannot remove other than from the Queue head methods");
+    }
+
+    @Override
+    public boolean retainAll(Collection<?> c) {
+        Objects.requireNonNull(c);
+        throw new UnsupportedOperationException("Cannot remove other than from the Queue head methods");
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        return new RingIterator();
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+    @Override
+    public void clear() {
+        read = 0;
+        write = -1;
+        size = 0;
+
+        Arrays.fill(backingArray, null);
+    }
+
+    @Override
+    public boolean contains(Object value) {
+        int count = size;
+        int position = read;
+
+        if (value == null) {
+            while (count > 0) {
+                if (backingArray[position] == null) {
+                    return true;
+                }
+                position = advance(position, backingArray.length);
+                count--;
+            }
+        } else {
+            while (count > 0) {
+                if (value.equals(backingArray[position])) {
+                    return true;
+                }
+                position = advance(position, backingArray.length);
+                count--;
+            }
+        }
+
+        return false;
+    }
+
+    //----- Internal implementation
+
+    private boolean isFull() {
+        return size == backingArray.length;
+    }
+
+    private static int advance(int value, int limit) {
+        return (++value) % limit;
+    }
+
+    //----- Ring Queue Iterator Implementation
+
+    private class RingIterator implements Iterator<E> {
+
+        private int expectedSize = size;
+        private int expectedReadIndex = read;
+
+        private E nextElement;
+        private int position;
+        private int remaining;
+        private E lastReturned;
+
+        public RingIterator() {
+            this.nextElement = peek();
+            this.position = read;
+            this.remaining = size;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return nextElement != null;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public E next() {
+            E entry = nextElement;
+
+            if (nextElement == null) {
+                throw new NoSuchElementException();
+            }
+            if (expectedSize != size || expectedReadIndex != read) {
+                throw new ConcurrentModificationException();
+            }
+
+            lastReturned = entry;
+
+            if (--remaining != 0) {
+                position = advance(position, backingArray.length);
+                nextElement = (E) backingArray[position];
+            } else {
+                nextElement = null;
+            }
+
+            return lastReturned;
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/SequenceNumber.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/SequenceNumber.java
new file mode 100644
index 0000000..79dc5bb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/SequenceNumber.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+/**
+ * A mutable sequence that represents an unsigned integer type underneath
+ */
+public class SequenceNumber extends Number implements Comparable<SequenceNumber> {
+
+    private static final long serialVersionUID = -1337181254740481576L;
+
+    private int sequence;
+
+    /**
+     * Create a new sequence starting at the given value.
+     *
+     * @param startValue
+     *      The starting value of this unsigned integer sequence
+     */
+    public SequenceNumber(int startValue) {
+        this.sequence = startValue;
+    }
+
+    /**
+     * Add one to the sequence value.
+     *
+     * @return this sequence.
+     */
+    public SequenceNumber increment() {
+        sequence++;
+        return this;
+    }
+
+    /**
+     * Subtract one to the sequence value.
+     *
+     * @return this sequence.
+     */
+    public SequenceNumber decrement() {
+        sequence--;
+        return this;
+    }
+
+    /**
+     * Add one to the sequence value.
+     *
+     * @return this sequence value prior to the increment.
+     */
+    public SequenceNumber getAndIncrement() {
+        return new SequenceNumber(sequence++);
+    }
+
+    /**
+     * Subtract one to the sequence value.
+     *
+     * @return this sequence value prior to the decrement.
+     */
+    public SequenceNumber getAndDecrement() {
+        return new SequenceNumber(sequence--);
+    }
+
+    @Override
+    public int intValue() {
+        return sequence;
+    }
+
+    @Override
+    public long longValue() {
+        return Integer.toUnsignedLong(sequence);
+    }
+
+    @Override
+    public float floatValue() {
+        return Float.intBitsToFloat(sequence);
+    }
+
+    @Override
+    public double doubleValue() {
+        return Double.longBitsToDouble(longValue());
+    }
+
+    @Override
+    public int compareTo(SequenceNumber other) {
+        return Integer.compareUnsigned(sequence, other.sequence);
+    }
+
+    public int compareTo(Number other) {
+        return Integer.compareUnsigned(sequence, other.intValue());
+    }
+
+    public int compareTo(int other) {
+        return Integer.compareUnsigned(sequence, other);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof SequenceNumber) {
+            return ((SequenceNumber) other).sequence == this.sequence;
+        }
+
+        return false;
+    }
+
+    public boolean equals(int other) {
+        return Integer.compareUnsigned(sequence, other) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Integer.hashCode(sequence);
+    }
+
+    @Override
+    public String toString() {
+        return Integer.toUnsignedString(sequence);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/SplayMap.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/SplayMap.java
new file mode 100644
index 0000000..cdd5097
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/SplayMap.java
@@ -0,0 +1,1213 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+/**
+ * Map class that is implemented using a Splay Tree and uses primitive integers as the keys
+ * for the specified value type.
+ *
+ * The splay tree is a specialized form of a binary search tree that is self balancing and
+ * provides faster access in general to frequently used items.  The splay tree serves well
+ * as an LRU cache of sorts where 80 percent of the accessed elements comes from 20 percent
+ * of the overall load in the {@link Map}.  The best case access time is generally O(long n)
+ * however it can be Theta(n) in a very worst case scenario.
+ *
+ * @param <E> The type stored in the map entries
+ */
+public class SplayMap<E> implements NavigableMap<UnsignedInteger, E> {
+
+    private static final UnsignedComparator COMPARATOR = new UnsignedComparator();
+
+    protected final RingQueue<SplayedEntry<E>> entryPool = new RingQueue<>(64);
+
+    /**
+     * Root node which can be null if the tree has no elements (size == 0)
+     */
+    protected SplayedEntry<E> root;
+
+    /**
+     * Current size of the splayed map tree.
+     */
+    protected int size;
+
+    protected int modCount;
+
+    @Override
+    public int size() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+    /**
+     * Gets the value of the element stored in the {@link Map} with the key (treated as an
+     * unsigned integer for comparison.
+     *
+     * As a side effect of calling this method the tree that comprises the Map can be modified
+     * to bring up the found key or the last accessed key if the key given is not in the {@link Map}.
+     * For entries at the root of the tree that match the given search key the method returns
+     * immediately without modifying the {@link Map}.
+     *
+     * @param key
+     *      the integer key value to search for in the {@link BottomUpSplayMap}.
+     *
+     * @return the value stored for the given key if found or null if not in the {@link Map}.
+     */
+    public E get(int key) {
+        if (root == null) {
+            return null;
+        } else if (root.key == key) {
+            return root.value;
+        } else {
+            root = splay(root, key);
+
+            if (root.key == key) {
+                return root.value;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    public E put(int key, E value) {
+        E oldValue = null;
+
+        if (root == null) {
+            root = entryPool.poll(SplayMap::createEmtry).initialize(key, value);
+        } else {
+            root = splay(root, key);
+            if (root.key == key) {
+                oldValue = root.value;
+                root.value = value;
+            } else {
+                final SplayedEntry<E> node = entryPool.poll(SplayMap::createEmtry).initialize(key, value);
+
+                if (compare(key, root.key) < 0) {
+                    shiftRootRightOf(node);
+                } else {
+                    shiftRootLeftOf(node);
+                }
+            }
+        }
+
+        if (oldValue == null) {
+            entryAdded(root);
+            size++;
+        }
+        modCount++;
+
+        return oldValue;
+    }
+
+    @Override
+    public E putIfAbsent(UnsignedInteger key, E value) {
+        return putIfAbsent(key.intValue(), value);
+    }
+
+    public E putIfAbsent(int key, E value) {
+        if (root == null) {
+            root = entryPool.poll(SplayMap::createEmtry).initialize(key, value);
+        } else {
+            root = splay(root, key);
+            if (root.key == key) {
+                return root.value;
+            } else {
+                final SplayedEntry<E> node = entryPool.poll(SplayMap::createEmtry).initialize(key, value);
+
+                if (compare(key, root.key) < 0) {
+                    shiftRootRightOf(node);
+                } else {
+                    shiftRootLeftOf(node);
+                }
+            }
+        }
+
+        entryAdded(root);
+        size++;
+        modCount++;
+
+        return null;
+    }
+
+    private void shiftRootRightOf(SplayedEntry<E> newRoot) {
+        newRoot.right = root;
+        newRoot.left = root.left;
+        if (newRoot.left != null) {
+            newRoot.left.parent = newRoot;
+        }
+        root.left = null;
+        root.parent = newRoot;
+        root = newRoot;
+    }
+
+    private void shiftRootLeftOf(SplayedEntry<E> newRoot) {
+        newRoot.left = root;
+        newRoot.right = root.right;
+        if (newRoot.right != null) {
+            newRoot.right.parent = newRoot;
+        }
+        root.right = null;
+        root.parent = newRoot;
+        root = newRoot;
+    }
+
+    public E remove(UnsignedInteger key) {
+        return remove(key.intValue());
+    }
+
+    public E remove(int key) {
+        if (root == null) {
+            return null;
+        }
+
+        root = splay(root, key);
+        if (root.key != key) {
+            return null;
+        }
+
+        final E removed = root.value;
+
+        // We splayed on the key and matched it so the root
+        // will now be the node matching that key.
+        delete(root);
+
+        return removed;
+    }
+
+    public boolean containsKey(int key) {
+        if (root == null) {
+            return false;
+        }
+
+        root = splay(root, key);
+        if (root.key == key) {
+            return true;
+        }
+
+        return false;
+    }
+
+    //----- Map interface implementation
+
+    @Override
+    public E put(UnsignedInteger key, E value) {
+        return put(key.intValue(), value);
+    }
+
+    @Override
+    public E get(Object key) {
+        return get(Number.class.cast(key).intValue());
+    }
+
+    @Override
+    public E remove(Object key) {
+        return remove(Number.class.cast(key).intValue());
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        Number numericKey = (Number) key;
+        return containsKey(numericKey.intValue());
+    }
+
+    @Override
+    public void clear() {
+        root = null;
+        size = 0;
+    }
+
+    @Override
+    public void putAll(Map<? extends UnsignedInteger, ? extends E> source) {
+        for (Entry<? extends UnsignedInteger, ? extends E> entry : source.entrySet()) {
+            put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        for (SplayedEntry<E> entry = firstEntry(root); entry != null; entry = successor(entry)) {
+            if (entry.valueEquals(value)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    // Once requested we will create an store a single instance to a collection
+    // with no state for each of the key, values and entries types.  Since the
+    // types are stateless the trivial race on create is not important to the
+    // eventual outcome of having a cached instance.
+
+    protected Set<UnsignedInteger> keySet;
+    protected Collection<E> values;
+    protected Set<Entry<UnsignedInteger, E>> entrySet;
+
+    @Override
+    public Set<UnsignedInteger> keySet() {
+        if (keySet == null) {
+            keySet = new SplayMapKeySet();
+        }
+        return keySet;
+    }
+
+    @Override
+    public Collection<E> values() {
+        if (values == null) {
+            values = new SplayMapValues();
+        }
+        return values;
+    }
+
+    @Override
+    public Set<Entry<UnsignedInteger, E>> entrySet() {
+        if (entrySet == null) {
+            entrySet = new SplayMapEntrySet();
+        }
+        return entrySet;
+    }
+
+    @Override
+    public void forEach(BiConsumer<? super UnsignedInteger, ? super E> action) {
+        Objects.requireNonNull(action);
+
+        for (SplayedEntry<E> entry = firstEntry(root); entry != null; entry = successor(entry)) {
+            action.accept(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * A specialized forEach implementation that accepts a {@link Consumer} function that will
+     * be called for each value in the {@link BottomUpSplayMap}.  This method can save overhead as it does not
+     * need to box the primitive key values into an object for the call to the provided function.
+     * Unless otherwise specified by the implementing class, actions are performed in the order of entry
+     * set iteration (if an iteration order is specified.)
+     *
+     * @param action
+     *      The action to be performed for each of the values in the {@link BottomUpSplayMap}.
+     */
+    public void forEach(Consumer<? super E> action) {
+        Objects.requireNonNull(action);
+
+        for (SplayedEntry<E> entry = firstEntry(root); entry != null; entry = successor(entry)) {
+            action.accept(entry.getValue());
+        }
+    }
+
+    @Override
+    public void replaceAll(BiFunction<? super UnsignedInteger, ? super E, ? extends E> function) {
+        Objects.requireNonNull(function, "The replacement function parameter cannot be null");
+
+        final int initialModCount = modCount;
+
+        for (SplayedEntry<E> entry = firstEntry(root); entry != null; entry = successor(entry)) {
+            entry.value = function.apply(entry.getKey(), entry.value);
+        }
+
+        if (modCount != initialModCount) {
+            throw new ConcurrentModificationException();
+        }
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+        Number numericKey = (Number) key;
+        return remove(numericKey.intValue(), value);
+    }
+
+    public boolean remove(int key, Object value) {
+        root = splay(root, key);
+        if (root == null || root.key != key || !Objects.equals(root.value, value)) {
+            return false;
+        } else {
+            delete(root);
+            return true;
+        }
+    }
+
+    @Override
+    public boolean replace(UnsignedInteger key, E oldValue, E newValue) {
+        return replace(key.intValue(), oldValue, newValue);
+    }
+
+    public boolean replace(int key, E oldValue, E newValue) {
+        root = splay(root, key);
+        if (root == null || root.key != key || !Objects.equals(root.value, oldValue)) {
+            return false;
+        } else {
+            root.setValue(newValue);
+            return true;
+        }
+    }
+
+    @Override
+    public E replace(UnsignedInteger key, E value) {
+        return replace(key.intValue(), value);
+    }
+
+    public E replace(int key, E value) {
+        root = splay(root, key);
+        if (root == null || root.key != key || root.value == null) {
+            return null;
+        } else {
+            return root.setValue(value);
+        }
+    }
+
+    //----- Extension points
+
+    protected void entryAdded(SplayedEntry<E> newEntry) {
+        // Nothing to do in the base class implementation.
+    }
+
+    protected void entryDeleted(SplayedEntry<E> deletedEntry) {
+        // Nothing to do in the base class implementation.
+    }
+
+    //----- Internal Implementation
+
+    /*
+     * Rotations of tree elements form the basis of search and balance operations
+     * within the tree during put, get and remove type operations.
+     *
+     *       y                                     x
+     *      / \     Zig (Right Rotation)          /  \
+     *     x   T3   – - – - – - – - - ->         T1   y
+     *    / \       < - - - - - - - - -              / \
+     *   T1  T2     Zag (Left Rotation)            T2   T3
+     *
+     */
+
+    private SplayedEntry<E> rightRotate(SplayedEntry<E> node) {
+        SplayedEntry<E> rotated = node.left;
+        node.left = rotated.right;
+        rotated.right = node;
+
+        // Reset the parent values for adjusted nodes.
+        rotated.parent = node.parent;
+        node.parent = rotated;
+        if (node.left != null) {
+            node.left.parent = node;
+        }
+
+        return rotated;
+    }
+
+    private SplayedEntry<E> leftRotate(SplayedEntry<E> node) {
+        SplayedEntry<E> rotated = node.right;
+        node.right = rotated.left;
+        rotated.left = node;
+
+        // Reset the parent values for adjusted nodes.
+        rotated.parent = node.parent;
+        node.parent = rotated;
+        if (node.right != null) {
+            node.right.parent = node;
+        }
+
+        return rotated;
+    }
+
+    /*
+     * The requested key if present is brought to the root of the tree.  If it is not
+     * present then the last accessed element (nearest match) will be brought to the root
+     * as it is likely it will be the next accessed or one of the neighboring nodes which
+     * reduces the search time for that cluster.
+     */
+    private SplayedEntry<E> splay(SplayedEntry<E> root, int key) {
+        if (root == null || root.key == key) {
+            return root;
+        }
+
+        SplayedEntry<E> lessThanKeyRoot = null;
+        SplayedEntry<E> lessThanKeyNode = null;
+        SplayedEntry<E> greaterThanKeyRoot = null;
+        SplayedEntry<E> greaterThanKeyNode = null;
+
+        while (true) {
+            if (compare(key, root.key) < 0) {
+                // Entry must be to the left of the current node so we bring that up
+                // and then work from there to see if we can find the key
+                if (root.left != null && compare(key, root.left.key) < 0) {
+                    root = rightRotate(root);
+                }
+
+                // Is there nowhere else to go, if so we are done.
+                if (root.left == null) {
+                    break;
+                }
+
+                // Haven't found it yet but we now know the current element is greater
+                // than the element we are looking for so it goes to the right tree.
+                if (greaterThanKeyRoot == null) {
+                    greaterThanKeyRoot = greaterThanKeyNode = root;
+                } else {
+                    greaterThanKeyNode.left = root;
+                    greaterThanKeyNode.left.parent = greaterThanKeyNode;
+                    greaterThanKeyNode = root;
+                }
+
+                root = root.left;
+                root.parent = null;
+            } else if (compare(key, root.key) > 0) {
+                // Entry must be to the right of the current node so we bring that up
+                // and then work from there to see if we can find the key
+                if (root.right != null && compare(key, root.right.key) > 0) {
+                    root = leftRotate(root);
+                }
+
+                // Is there nowhere else to go, if so we are done.
+                if (root.right == null) {
+                    break;
+                }
+
+                // Haven't found it yet but we now know the current element is less
+                // than the element we are looking for so it goes to the left tree.
+                if (lessThanKeyRoot == null) {
+                    lessThanKeyRoot = lessThanKeyNode = root;
+                } else {
+                    lessThanKeyNode.right = root;
+                    lessThanKeyNode.right.parent = lessThanKeyNode;
+                    lessThanKeyNode = root;
+                }
+
+                root = root.right;
+                root.parent = null;
+            } else {
+                break; // Found it
+            }
+        }
+
+        // Reassemble the tree from the left, right and middle the assembled nodes in the
+        // left and right should have their last element either nulled out or linked to the
+        // remaining items middle tree
+        if (lessThanKeyRoot == null) {
+            lessThanKeyRoot = root.left;
+        } else {
+            lessThanKeyNode.right = root.left;
+            if (lessThanKeyNode.right != null) {
+                lessThanKeyNode.right.parent = lessThanKeyNode;
+            }
+        }
+
+        if (greaterThanKeyRoot == null) {
+            greaterThanKeyRoot = root.right;
+        } else {
+            greaterThanKeyNode.left = root.right;
+            if (greaterThanKeyNode.left != null) {
+                greaterThanKeyNode.left.parent = greaterThanKeyNode;
+            }
+        }
+
+        // The found or last accessed element is now rooted to the splayed
+        // left and right trees and returned as the new tree.
+        root.left = lessThanKeyRoot;
+        if (root.left != null) {
+            root.left.parent = root;
+        }
+        root.right = greaterThanKeyRoot;
+        if (root.right != null) {
+            root.right.parent = root;
+        }
+
+        return root;
+    }
+
+    protected void delete(SplayedEntry<E> node) {
+        final SplayedEntry<E> grandparent = node.parent;
+        SplayedEntry<E> replacement = node.right;
+
+        if (node.left != null) {
+            replacement = splay(node.left, node.key);
+            replacement.right = node.right;
+        }
+
+        if (replacement != null) {
+            replacement.parent = grandparent;
+        }
+
+        if (grandparent != null) {
+            if (grandparent.left == node) {
+                grandparent.left = replacement;
+            } else {
+                grandparent.right = replacement;
+            }
+        } else {
+            root = replacement;
+        }
+
+        // Clear node before moving to cache
+        node.left = node.right = node.parent = null;
+        entryPool.offer(node);
+
+        entryDeleted(node);
+
+        size--;
+        modCount++;
+    }
+
+    private SplayedEntry<E> firstEntry(SplayedEntry<E> node) {
+        SplayedEntry<E> firstEntry = node;
+        if (firstEntry != null) {
+            while (firstEntry.left != null) {
+                firstEntry = firstEntry.left;
+            }
+        }
+
+        return firstEntry;
+    }
+
+    private SplayedEntry<E> lastEntry(SplayedEntry<E> node) {
+        SplayedEntry<E> lastEntry = node;
+        if (lastEntry != null) {
+            while (lastEntry.right != null) {
+                lastEntry = lastEntry.right;
+            }
+        }
+
+        return lastEntry;
+    }
+
+    private SplayedEntry<E> successor(SplayedEntry<E> node) {
+        if (node == null) {
+            return null;
+        } else if (node.right != null) {
+            // Walk to bottom of tree from this node's right child.
+            SplayedEntry<E> result = node.right;
+            while (result.left != null) {
+                result = result.left;
+            }
+
+            return result;
+        } else {
+            SplayedEntry<E> parent = node.parent;
+            SplayedEntry<E> child = node;
+            while (parent != null && child == parent.right) {
+                child = parent;
+                parent = parent.parent;
+            }
+
+            return parent;
+        }
+    }
+
+    private SplayedEntry<E> predecessor(SplayedEntry<E> node) {
+        if (node == null) {
+            return null;
+        } else if (node.left != null) {
+            // Walk to bottom of tree from this node's left child.
+            SplayedEntry<E> result = node.left;
+            while (result.right != null) {
+                result = result.right;
+            }
+
+            return result;
+        } else {
+            SplayedEntry<E> parent = node.parent;
+            SplayedEntry<E> child = node;
+            while (parent != null && child == parent.left) {
+                child = parent;
+                parent = parent.parent;
+            }
+
+            return parent;
+        }
+    }
+
+    private static int compare(int lhs, int rhs) {
+        return Integer.compareUnsigned(lhs, rhs);
+    }
+
+    private static <E> SplayedEntry<E> createEmtry() {
+        return new SplayedEntry<>();
+    }
+
+    //----- Map Iterator implementation for EntrySet, KeySet and Values collections
+
+    // Base class iterator that can be used for the collections returned from the Map
+    private abstract class SplayMapIterator<T> implements Iterator<T> {
+
+        private SplayedEntry<E> nextNode;
+        private SplayedEntry<E> lastReturned;
+
+        private int expectedModCount;
+
+        public SplayMapIterator(SplayedEntry<E> startAt) {
+            this.nextNode = startAt;
+            this.expectedModCount = SplayMap.this.modCount;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return nextNode != null;
+        }
+
+        protected SplayedEntry<E> nextNode() {
+            final SplayedEntry<E> entry = nextNode;
+
+            if (nextNode == null) {
+                throw new NoSuchElementException();
+            }
+            if (expectedModCount != SplayMap.this.modCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            nextNode = successor(nextNode);
+            lastReturned = entry;
+
+            return lastReturned;
+        }
+
+        // Unused as of now but can be used for NavigableMap amongst other things
+        @SuppressWarnings("unused")
+        protected SplayedEntry<E> previousNode() {
+            final SplayedEntry<E> entry = nextNode;
+
+            if (nextNode == null) {
+                throw new NoSuchElementException();
+            }
+            if (expectedModCount != SplayMap.this.modCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            nextNode = predecessor(nextNode);
+            lastReturned = entry;
+
+            return lastReturned;
+        }
+
+        @Override
+        public void remove() {
+            if (lastReturned == null) {
+                throw new IllegalStateException();
+            }
+            if (modCount != expectedModCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            delete(lastReturned);
+
+            expectedModCount = modCount;
+            lastReturned = null;
+        }
+    }
+
+    private class SplayMapEntryIterator extends SplayMapIterator<Entry<UnsignedInteger, E>> {
+
+        public SplayMapEntryIterator(SplayedEntry<E> startAt) {
+            super(startAt);
+        }
+
+        @Override
+        public Entry<UnsignedInteger, E> next() {
+            return nextNode();
+        }
+    }
+
+    private class SplayMapKeyIterator extends SplayMapIterator<UnsignedInteger> {
+
+        public SplayMapKeyIterator(SplayedEntry<E> startAt) {
+            super(startAt);
+        }
+
+        @Override
+        public UnsignedInteger next() {
+            return nextNode().getKey();
+        }
+    }
+
+    private class SplayMapValueIterator extends SplayMapIterator<E> {
+
+        public SplayMapValueIterator(SplayedEntry<E> startAt) {
+            super(startAt);
+        }
+
+        @Override
+        public E next() {
+            return nextNode().getValue();
+        }
+    }
+
+    //----- Splay Map Collection types
+
+    private final class SplayMapValues extends AbstractCollection<E> {
+
+        @Override
+        public Iterator<E> iterator() {
+            return new SplayMapValueIterator(firstEntry(root));
+        }
+
+        @Override
+        public int size() {
+            return SplayMap.this.size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return SplayMap.this.containsValue(o);
+        }
+
+        @Override
+        public boolean remove(Object target) {
+            for (SplayedEntry<E> e = firstEntry(root); e != null; e = successor(e)) {
+                if (e.valueEquals(target)) {
+                    delete(e);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void clear() {
+            SplayMap.this.clear();
+        }
+    }
+
+    private final class SplayMapKeySet extends AbstractSet<UnsignedInteger> {
+
+        @Override
+        public Iterator<UnsignedInteger> iterator() {
+            return new SplayMapKeyIterator(firstEntry(root));
+        }
+
+        @Override
+        public int size() {
+            return SplayMap.this.size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return SplayMap.this.containsKey(o);
+        }
+
+        @Override
+        public boolean remove(Object target) {
+            for (SplayedEntry<E> e = firstEntry(root); e != null; e = successor(e)) {
+                if (e.keyEquals(target)) {
+                    delete(e);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void clear() {
+            SplayMap.this.clear();
+        }
+    }
+
+    private final class SplayMapEntrySet extends AbstractSet<Entry<UnsignedInteger, E>> {
+
+        @Override
+        public Iterator<Entry<UnsignedInteger, E>> iterator() {
+            return new SplayMapEntryIterator(firstEntry(root));
+        }
+
+        @Override
+        public int size() {
+            return SplayMap.this.size;
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            if (!(o instanceof Map.Entry) || SplayMap.this.root == null) {
+                return false;
+            }
+
+            for (SplayedEntry<E> e = firstEntry(root); e != null; e = successor(e)) {
+                if (e.equals(o)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean remove(Object target) {
+            if (!(target instanceof Entry)) {
+                throw new IllegalArgumentException("value provided is not an Entry type.");
+            }
+
+            for (SplayedEntry<E> e = firstEntry(root); e != null; e = successor(e)) {
+                if (e.equals(target)) {
+                    delete(e);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void clear() {
+            SplayMap.this.clear();
+        }
+    }
+
+    //----- Map Entry node for the Splay Map
+
+    protected static final class SplayedEntry<E> implements Map.Entry<UnsignedInteger, E>{
+
+        SplayedEntry<E> left;
+        SplayedEntry<E> right;
+        SplayedEntry<E> parent;
+
+        int key;
+        E value;
+
+        // Insertion order chain used by LinkedSplayMap
+        SplayedEntry<E> linkNext;
+        SplayedEntry<E> linkPrev;
+
+        public SplayedEntry() {
+            initialize(key, value);
+        }
+
+        public SplayedEntry<E> initialize(int key, E value) {
+            this.key = key;
+            this.value = value;
+            // Node is circular list to start.
+            this.linkNext = this;
+            this.linkPrev = this;
+
+            return this;
+        }
+
+        public int getIntKey() {
+            return key;
+        }
+
+        @Override
+        public UnsignedInteger getKey() {
+            return UnsignedInteger.valueOf(key);
+        }
+
+        @Override
+        public E getValue() {
+            return value;
+        }
+
+        @Override
+        public E setValue(E value) {
+            E oldValue = this.value;
+            this.value = value;
+            return oldValue;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof Map.Entry)) {
+                return false;
+            }
+
+            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
+
+            return keyEquals(e.getKey()) && valueEquals(e.getValue());
+        }
+
+        @Override
+        public int hashCode() {
+            return key ^ (value == null ? 0 : value.hashCode());
+        }
+
+        @Override
+        public String toString() {
+            return "Node:{" + key + "," + value + "}";
+        }
+
+        boolean keyEquals(Object other) {
+            if (!(other instanceof Number)) {
+                return false;
+            }
+
+            return key == ((Number) other).intValue();
+        }
+
+        boolean valueEquals(Object other) {
+            return value != null ? value.equals(other) : other == null;
+        }
+    }
+
+    public class ImmutableSplayMapEntry implements Map.Entry<UnsignedInteger, E> {
+
+        private final SplayedEntry<E> entry;
+
+        public ImmutableSplayMapEntry(SplayedEntry<E> entry) {
+            this.entry = entry;
+        }
+
+        @Override
+        public UnsignedInteger getKey() {
+            return entry.getKey();
+        }
+
+        public int getPrimitiveKey() {
+            return entry.getIntKey();
+        }
+
+        @Override
+        public E getValue() {
+            return entry.getValue();
+        }
+
+        @Override
+        public E setValue(E value) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    protected ImmutableSplayMapEntry export(SplayedEntry<E> entry) {
+        return entry == null ? null : new ImmutableSplayMapEntry(entry);
+    }
+
+    //----- Unsigned Integer comparator for Navigable Maps
+
+    private static final class UnsignedComparator implements Comparator<UnsignedInteger> {
+
+        @Override
+        public int compare(UnsignedInteger uint1, UnsignedInteger uint2) {
+            return uint1.compareTo(uint2);
+        }
+    }
+
+    //----- Navigable and Sorted Map implementation methods
+
+    @Override
+    public Comparator<? super UnsignedInteger> comparator() {
+        return COMPARATOR;
+    }
+
+    @Override
+    public UnsignedInteger firstKey() {
+        return isEmpty() ? null : firstEntry(root).getKey();
+    }
+
+    @Override
+    public UnsignedInteger lastKey() {
+        return isEmpty() ? null : lastEntry(root).getKey();
+    }
+
+    @Override
+    public ImmutableSplayMapEntry firstEntry() {
+        return export(firstEntry(root));
+    }
+
+    @Override
+    public ImmutableSplayMapEntry lastEntry() {
+        return export(lastEntry(root));
+    }
+
+    @Override
+    public ImmutableSplayMapEntry pollFirstEntry() {
+        SplayedEntry<E> firstEntry = firstEntry(root);
+        if (firstEntry != null) {
+            delete(firstEntry);
+        }
+        return export(firstEntry);
+    }
+
+    @Override
+    public ImmutableSplayMapEntry pollLastEntry() {
+        SplayedEntry<E> lastEntry = lastEntry(root);
+        if (lastEntry != null) {
+            delete(lastEntry);
+        }
+        return export(lastEntry);
+    }
+
+    @Override
+    public ImmutableSplayMapEntry lowerEntry(UnsignedInteger key) {
+        return export(lowerEntry(key.intValue()));
+    }
+
+    @Override
+    public UnsignedInteger lowerKey(UnsignedInteger key) {
+        final SplayedEntry<E> result = lowerEntry(key.intValue());
+
+        return result == null ? null : result.getKey();
+    }
+
+    private SplayedEntry<E> lowerEntry(int key) {
+        root = splay(root, key);
+
+        while (root != null) {
+            if (compare(root.getIntKey(), key) >= 0) {
+                root = predecessor(root);
+            } else {
+                break;
+            }
+        }
+
+        return root;
+    }
+
+    @Override
+    public ImmutableSplayMapEntry higherEntry(UnsignedInteger key) {
+        return export(higherEntry(key.intValue()));
+    }
+
+    @Override
+    public UnsignedInteger higherKey(UnsignedInteger key) {
+        final SplayedEntry<E> result = higherEntry(key.intValue());
+
+        return result == null ? null : result.getKey();
+    }
+
+    private SplayedEntry<E> higherEntry(int key) {
+        root = splay(root, key);
+
+        while (root != null) {
+            if (compare(root.getIntKey(), key) <= 0) {
+                root = successor(root);
+            } else {
+                break;
+            }
+        }
+
+        return root;
+    }
+
+    @Override
+    public ImmutableSplayMapEntry floorEntry(UnsignedInteger key) {
+        return export(floorEntry(key.intValue()));
+    }
+
+    @Override
+    public UnsignedInteger floorKey(UnsignedInteger key) {
+        final SplayedEntry<E> result = floorEntry(key.intValue());
+
+        return result == null ? null : result.getKey();
+    }
+
+    private SplayedEntry<E> floorEntry(int key) {
+        root = splay(root, key);
+
+        while (root != null) {
+            if (compare(root.getIntKey(), key) > 0) {
+                root = predecessor(root);
+            } else {
+                break;
+            }
+        }
+
+        return root;
+    }
+
+    @Override
+    public ImmutableSplayMapEntry ceilingEntry(UnsignedInteger key) {
+        return export(ceilingEntry(key.intValue()));
+    }
+
+    @Override
+    public UnsignedInteger ceilingKey(UnsignedInteger key) {
+        final SplayedEntry<E> result = ceilingEntry(key.intValue());
+
+        return result == null ? null : result.getKey();
+    }
+
+    private SplayedEntry<E> ceilingEntry(int key) {
+        root = splay(root, key);
+
+        while (root != null) {
+            if (compare(root.getIntKey(), key) < 0) {
+                root = successor(root);
+            } else {
+                break;
+            }
+        }
+
+        return root;
+    }
+
+    @Override
+    public NavigableMap<UnsignedInteger, E> descendingMap() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public NavigableSet<UnsignedInteger> navigableKeySet() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public NavigableSet<UnsignedInteger> descendingKeySet() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public NavigableMap<UnsignedInteger, E> subMap(UnsignedInteger fromKey, boolean fromInclusive, UnsignedInteger toKey, boolean toInclusive) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public NavigableMap<UnsignedInteger, E> headMap(UnsignedInteger toKey, boolean inclusive) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public NavigableMap<UnsignedInteger, E> tailMap(UnsignedInteger fromKey, boolean inclusive) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public SortedMap<UnsignedInteger, E> subMap(UnsignedInteger fromKey, UnsignedInteger toKey) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public SortedMap<UnsignedInteger, E> headMap(UnsignedInteger toKey) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public SortedMap<UnsignedInteger, E> tailMap(UnsignedInteger fromKey) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/StringUtils.java b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/StringUtils.java
new file mode 100644
index 0000000..56d6c33
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/util/StringUtils.java
@@ -0,0 +1,215 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+
+public class StringUtils {
+
+    public static Symbol[] toSymbolArray(String[] stringArray) {
+        Symbol[] result = null;
+
+        if (stringArray != null) {
+            result = new Symbol[stringArray.length];
+            for (int i = 0; i < stringArray.length; ++i) {
+                result[i] = Symbol.valueOf(stringArray[i]);
+            }
+        }
+
+        return result;
+    }
+
+    public static String[] toStringArray(Symbol[] symbolArray) {
+        String[] result = null;
+
+        if (symbolArray != null) {
+            result = new String[symbolArray.length];
+            for (int i = 0; i < symbolArray.length; ++i) {
+                result[i] = symbolArray[i].toString();
+            }
+        }
+
+        return result;
+    }
+
+    public static Map<Symbol, Object> toSymbolKeyedMap(Map<String, Object> stringsMap) {
+        final Map<Symbol, Object> result;
+
+        if (stringsMap != null) {
+            result = new HashMap<>(stringsMap.size());
+            stringsMap.forEach((key, value) -> {
+                result.put(Symbol.valueOf(key), value);
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Map<String, Object> toStringKeyedMap(Map<Symbol, Object> symbolMap) {
+        Map<String, Object> result;
+
+        if (symbolMap != null) {
+            result = new LinkedHashMap<>(symbolMap.size());
+            symbolMap.forEach((key, value) -> {
+                result.put(key.toString(), value);
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Symbol[] toSymbolArray(Collection<String> stringsSet) {
+        final Symbol[] result;
+
+        if (stringsSet != null) {
+            result = new Symbol[stringsSet.size()];
+            int index = 0;
+            for (String entry : stringsSet) {
+                result[index++] = Symbol.valueOf(entry);
+            }
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Set<Symbol> toSymbolSet(Collection<String> stringsSet) {
+        final Set<Symbol> result;
+
+        if (stringsSet != null) {
+            result = new LinkedHashSet<>(stringsSet.size());
+            stringsSet.forEach((entry) -> {
+                result.add(Symbol.valueOf(entry));
+            });
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    public static Set<String> toStringSet(Symbol[] symbols) {
+        Set<String> result;
+
+        if (symbols != null) {
+            result = new LinkedHashSet<>(symbols.length);
+            for (Symbol symbol : symbols) {
+                result.add(symbol.toString());
+            }
+        } else {
+            result = null;
+        }
+
+        return result;
+    }
+
+    /**
+     * Converts the Binary to a quoted string.
+     *
+     * @param buffer
+     *        the {@link Binary} to convert into String format.
+     * @param stringLength
+     *        the maximum length of stringified content (excluding the quotes, and truncated indicator)
+     * @param appendIfTruncated
+     *        appends "...(truncated)" if not all of the payload is present in the string
+     *
+     * @return the converted string
+     */
+    public static String toQuotedString(final Binary buffer, final int stringLength, final boolean appendIfTruncated) {
+        if (buffer == null) {
+            return "\"\"";
+        }
+
+        ProtonBuffer wrapped = ProtonByteBufferAllocator.DEFAULT.wrap(buffer.getArray(), buffer.getArrayOffset(), buffer.getLength());
+
+        return toQuotedString(wrapped, stringLength, appendIfTruncated);
+    }
+
+    /**
+     * Converts the ProtonBuffer to a quoted string.
+     *
+     * @param buffer
+     *        the {@link ProtonBuffer} to convert into String format.
+     * @param stringLength
+     *        the maximum length of stringified content (excluding the quotes, and truncated indicator)
+     * @param appendIfTruncated
+     *        appends "...(truncated)" if not all of the payload is present in the string
+     *
+     * @return the converted string
+     */
+    public static String toQuotedString(final ProtonBuffer buffer, final int stringLength, final boolean appendIfTruncated) {
+        if (buffer == null) {
+            return "\"\"";
+        }
+
+        StringBuilder str = new StringBuilder();
+        str.append("\"");
+
+        final int byteToRead = buffer.getReadableBytes();
+        int size = 0;
+        boolean truncated = false;
+
+        for (int i = 0; i < byteToRead; ++i) {
+            byte c = buffer.getByte(i);
+
+            if (c > 31 && c < 127 && c != '\\') {
+                if (size + 1 <= stringLength) {
+                    size += 1;
+                    str.append((char) c);
+                } else {
+                    truncated = true;
+                    break;
+                }
+            } else {
+                if (size + 4 <= stringLength) {
+                    size += 4;
+                    str.append(String.format("\\x%02x", c));
+                } else {
+                    truncated = true;
+                    break;
+                }
+            }
+        }
+
+        str.append("\"");
+
+        if (truncated && appendIfTruncated) {
+            str.append("...(truncated)");
+        }
+
+        return str.toString();
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/logging/NoOpProtonLogger.java b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/NoOpProtonLogger.java
new file mode 100644
index 0000000..91a9ece
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/NoOpProtonLogger.java
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.logging;
+
+/**
+ * Simple proton logger implementation that performs no logging.
+ */
+public class NoOpProtonLogger implements ProtonLogger {
+
+    public static final NoOpProtonLogger INSTANCE = new NoOpProtonLogger();
+
+    private NoOpProtonLogger() {
+    }
+
+    @Override
+    public String getName() {
+        return null;
+    }
+
+    @Override
+    public boolean isTraceEnabled() {
+        return false;
+    }
+
+    @Override
+    public void trace(String message) {
+    }
+
+    @Override
+    public void trace(String message, Object value) {
+    }
+
+    @Override
+    public void trace(String message, Object value1, Object value2) {
+    }
+
+    @Override
+    public void trace(String message, Object... arguments) {
+    }
+
+    @Override
+    public boolean isDebugEnabled() {
+        return false;
+    }
+
+    @Override
+    public void debug(String message) {
+    }
+
+    @Override
+    public void debug(String message, Object value) {
+    }
+
+    @Override
+    public void debug(String message, Object value1, Object value2) {
+    }
+
+    @Override
+    public void debug(String message, Object... arguments) {
+    }
+
+    @Override
+    public boolean isInfoEnabled() {
+        return false;
+    }
+
+    @Override
+    public void info(String message) {
+    }
+
+    @Override
+    public void info(String message, Object value) {
+    }
+
+    @Override
+    public void info(String message, Object value1, Object value2) {
+    }
+
+    @Override
+    public void info(String message, Object... arguments) {
+    }
+
+    @Override
+    public boolean isWarnEnabled() {
+        return false;
+    }
+
+    @Override
+    public void warn(String message) {
+    }
+
+    @Override
+    public void warn(String message, Object value) {
+    }
+
+    @Override
+    public void warn(String message, Object value1, Object value2) {
+    }
+
+    @Override
+    public void warn(String message, Object... arguments) {
+    }
+
+    @Override
+    public boolean isErrorEnabled() {
+        return false;
+    }
+
+    @Override
+    public void error(String message) {
+    }
+
+    @Override
+    public void erro(String message, Object value) {
+    }
+
+    @Override
+    public void error(String message, Object value1, Object value2) {
+    }
+
+    @Override
+    public void error(String message, Object... arguments) {
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/logging/NoOpProtonLoggerFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/NoOpProtonLoggerFactory.java
new file mode 100644
index 0000000..09b88ac
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/NoOpProtonLoggerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.logging;
+
+/**
+ * ProtonLoggerFactory implementation that create do nothing loggers.
+ */
+public class NoOpProtonLoggerFactory extends ProtonLoggerFactory {
+
+    public static final NoOpProtonLoggerFactory INSTANCE = new NoOpProtonLoggerFactory();
+
+    private NoOpProtonLoggerFactory() {
+    }
+
+    @Override
+    protected ProtonLogger createLoggerWrapper(String name) {
+        return NoOpProtonLogger.INSTANCE;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/logging/ProtonLogger.java b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/ProtonLogger.java
new file mode 100644
index 0000000..bbc2b5d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/ProtonLogger.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.logging;
+
+/**
+ * Proton Logger abstraction
+ * <p>
+ * This interface provides an abstraction to be used around third party
+ * Logging frameworks such as slf4j, log4j etc.
+ */
+public interface ProtonLogger {
+
+    public String getName();
+
+    public boolean isTraceEnabled();
+
+    public void trace(String message);
+
+    public void trace(String message, Object value);
+
+    public void trace(String message, Object value1, Object value2);
+
+    public void trace(String message, Object... arguments);
+
+    public boolean isDebugEnabled();
+
+    public void debug(String message);
+
+    public void debug(String message, Object value);
+
+    public void debug(String message, Object value1, Object value2);
+
+    public void debug(String message, Object... arguments);
+
+    public boolean isInfoEnabled();
+
+    public void info(String message);
+
+    public void info(String message, Object value);
+
+    public void info(String message, Object value1, Object value2);
+
+    public void info(String message, Object... arguments);
+
+    public boolean isWarnEnabled();
+
+    public void warn(String message);
+
+    public void warn(String message, Object value);
+
+    public void warn(String message, Object value1, Object value2);
+
+    public void warn(String message, Object... arguments);
+
+    public boolean isErrorEnabled();
+
+    public void error(String message);
+
+    public void erro(String message, Object value);
+
+    public void error(String message, Object value1, Object value2);
+
+    public void error(String message, Object... arguments);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/logging/ProtonLoggerFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/ProtonLoggerFactory.java
new file mode 100644
index 0000000..f0562e1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/ProtonLoggerFactory.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.logging;
+
+/**
+ * Factory used to create Proton Logger abstractions
+ */
+public abstract class ProtonLoggerFactory {
+
+    private static volatile ProtonLoggerFactory loggerFactory;
+
+    /**
+     * Returns a {@link ProtonLoggerFactory} instance.
+     * <p>
+     * The factory returned depends on the configuration and available
+     * logger implementations at the time this method is called.  If a
+     * custom ProtonLoggerFactory is configured than that instance is
+     * returned, otherwise this class will attempt to find a factory for
+     * the slf4j logger library.
+     *
+     * @return a {@link ProtonLoggerFactory} that will be used by this library.
+     */
+    public static ProtonLoggerFactory getLoggerFactory() {
+        if (loggerFactory == null) {
+            loggerFactory = findSupportedLoggingFramework();
+        }
+
+        return loggerFactory;
+    }
+
+    /**
+     * Configure Proton with a custom ProtonLoggerFactory implementation which will
+     * be used by the Proton classes when logging library events.
+     *
+     * @param factory
+     *      The {@link ProtonLoggerFactory} to use when loggers are requested.
+     *
+     * @throws IllegalArgumentException if the factory given is null.
+     */
+    public static void setLoggerFactory(ProtonLoggerFactory factory) {
+        if (loggerFactory == null) {
+            throw new IllegalArgumentException("Cannot configure the logger factory as null");
+        }
+
+        ProtonLoggerFactory.loggerFactory = factory;
+    }
+
+    public static ProtonLogger getLogger(Class<?> clazz) {
+        return getLoggerFactory().createLoggerWrapper(clazz.getName());
+    }
+
+    public static ProtonLogger getLogger(String name) {
+        return getLoggerFactory().createLoggerWrapper(name);
+    }
+
+    protected abstract ProtonLogger createLoggerWrapper(String name);
+
+    //----- Logger search ----------------------------------------------------//
+
+    private static ProtonLoggerFactory findSupportedLoggingFramework() {
+        ProtonLoggerFactory factory = null;
+
+        try {
+            factory = Slf4JLoggerFactory.findLoggerFactory();
+            factory.createLoggerWrapper(ProtonLoggerFactory.class.getName()).debug(
+                "SLF4J found and will be used as the logging framework");
+        } catch (Throwable t1) {
+            factory = NoOpProtonLoggerFactory.INSTANCE;
+        }
+
+        return factory;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/logging/Slf4JLoggerFactory.java b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/Slf4JLoggerFactory.java
new file mode 100644
index 0000000..36b3c96
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/Slf4JLoggerFactory.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.logging;
+
+import org.slf4j.LoggerFactory;
+import org.slf4j.helpers.NOPLoggerFactory;
+
+/**
+ * Slf4j adapter class used to proxy calls to the slf4j logger
+ * factory and create ProtonLogger wrappers around the Loggers
+ * for that library.
+ */
+public class Slf4JLoggerFactory extends ProtonLoggerFactory {
+
+    /*
+     * Must be created via the static find method.
+     */
+    private Slf4JLoggerFactory() {
+    }
+
+    public static ProtonLoggerFactory findLoggerFactory() {
+        // We don't support the NO-OP logger and instead will fall back to our own variant
+        if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) {
+            throw new NoClassDefFoundError("Slf4j NOPLoggerFactory is not supported by this library");
+        }
+
+        return new Slf4JLoggerFactory();
+    }
+
+    @Override
+    protected ProtonLogger createLoggerWrapper(String name) {
+        return new Slf4JLoggerWrapper(LoggerFactory.getLogger(name));
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/logging/Slf4JLoggerWrapper.java b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/Slf4JLoggerWrapper.java
new file mode 100644
index 0000000..c18a1a0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/logging/Slf4JLoggerWrapper.java
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.logging;
+
+import org.slf4j.Logger;
+
+/**
+ * A Wrapper around an Slf4J Logger used to proxy logging calls to that
+ * framework when it is available.
+ */
+public class Slf4JLoggerWrapper implements ProtonLogger {
+
+    private final transient Logger logger;
+
+    Slf4JLoggerWrapper(Logger logger) {
+        this.logger = logger;
+    }
+
+    @Override
+    public String getName() {
+        return logger.getName();
+    }
+
+    @Override
+    public boolean isTraceEnabled() {
+        return logger.isTraceEnabled();
+    }
+
+    @Override
+    public void trace(String message) {
+        logger.trace(message);
+    }
+
+    @Override
+    public void trace(String message, Object value) {
+        logger.trace(message, value);
+    }
+
+    @Override
+    public void trace(String message, Object value1, Object value2) {
+        logger.trace(message, value1, value2);
+    }
+
+    @Override
+    public void trace(String message, Object... arguments) {
+        logger.trace(message, arguments);
+    }
+
+    @Override
+    public boolean isDebugEnabled() {
+        return logger.isDebugEnabled();
+    }
+
+    @Override
+    public void debug(String message) {
+        logger.debug(message);
+    }
+
+    @Override
+    public void debug(String message, Object value) {
+        logger.debug(message, value);
+    }
+
+    @Override
+    public void debug(String message, Object value1, Object value2) {
+        logger.debug(message, value1, value2);
+    }
+
+    @Override
+    public void debug(String message, Object... arguments) {
+        logger.debug(message, arguments);
+    }
+
+    @Override
+    public boolean isInfoEnabled() {
+        return logger.isInfoEnabled();
+    }
+
+    @Override
+    public void info(String message) {
+        logger.info(message);
+    }
+
+    @Override
+    public void info(String message, Object value) {
+        logger.info(message, value);
+    }
+
+    @Override
+    public void info(String message, Object value1, Object value2) {
+        logger.info(message, value1, value2);
+    }
+
+    @Override
+    public void info(String message, Object... arguments) {
+        logger.info(message, arguments);
+    }
+
+    @Override
+    public boolean isWarnEnabled() {
+        return logger.isWarnEnabled();
+    }
+
+    @Override
+    public void warn(String message) {
+        logger.warn(message);
+    }
+
+    @Override
+    public void warn(String message, Object value) {
+        logger.warn(message, value);
+    }
+
+    @Override
+    public void warn(String message, Object value1, Object value2) {
+        logger.warn(message, value1, value2);
+    }
+
+    @Override
+    public void warn(String message, Object... arguments) {
+        logger.warn(message, arguments);
+    }
+
+    @Override
+    public boolean isErrorEnabled() {
+        return logger.isErrorEnabled();
+    }
+
+    @Override
+    public void error(String message) {
+        logger.error(message);
+    }
+
+    @Override
+    public void erro(String message, Object value) {
+        logger.error(message, value);
+    }
+
+    @Override
+    public void error(String message, Object value1, Object value2) {
+        logger.error(message, value1, value2);
+    }
+
+    @Override
+    public void error(String message, Object... arguments) {
+        logger.error(message, arguments);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/Binary.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Binary.java
new file mode 100644
index 0000000..bcc061c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Binary.java
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import java.nio.ByteBuffer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+
+public final class Binary {
+
+    private final ProtonBuffer buffer;
+    private int hashCode;
+
+    public Binary() {
+        this((ProtonBuffer) null);
+    }
+
+    public Binary(ProtonBuffer buffer) {
+        this.buffer = buffer;
+    }
+
+    public Binary(final byte[] data) {
+        this(data, 0, data.length);
+    }
+
+    public Binary(final byte[] data, final int offset, final int length) {
+        this.buffer = ProtonByteBufferAllocator.DEFAULT.wrap(data, offset, length);
+    }
+
+    public Binary copy() {
+        if (buffer == null) {
+            return new Binary();
+        } else {
+            return new Binary(buffer.copy());
+        }
+    }
+
+    public byte[] arrayCopy() {
+        byte[] dataCopy = null;
+        if (buffer != null) {
+            dataCopy = new byte[buffer.getReadableBytes()];
+            buffer.getBytes(buffer.getReadIndex(), dataCopy);
+        }
+
+        return dataCopy;
+    }
+
+    public ByteBuffer asByteBuffer() {
+        return buffer != null ? buffer.toByteBuffer() : null;
+    }
+
+    public ProtonBuffer asProtonBuffer() {
+        return buffer;
+    }
+
+    @Override
+    public final int hashCode() {
+        int hc = hashCode;
+        if (hc == 0 && buffer != null) {
+            hashCode = buffer.hashCode();
+        }
+        return hc;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Binary other = (Binary) o;
+        if (getLength() != other.getLength()) {
+            return false;
+        }
+
+        if (buffer == null) {
+            return other.buffer == null;
+        }
+
+        return buffer.equals(other.buffer);
+    }
+
+    public boolean hasArray() {
+        return buffer != null ? buffer.hasArray() : false;
+    }
+
+    public int getArrayOffset() {
+        return buffer != null ? buffer.getArrayOffset() : 0;
+    }
+
+    public byte[] getArray() {
+        return buffer != null ? buffer.getArray() : null;
+    }
+
+    public int getLength() {
+        return buffer != null ? buffer.getReadableBytes() : 0;
+    }
+
+    @Override
+    public String toString() {
+        if (buffer == null) {
+            return "";
+        }
+
+        StringBuilder str = new StringBuilder();
+
+        for (int i = 0; i < getLength(); i++) {
+            byte c = buffer.getByte(i);
+
+            if (c > 31 && c < 127 && c != '\\') {
+                str.append((char) c);
+            } else {
+                str.append(String.format("\\x%02x", c));
+            }
+        }
+
+        return str.toString();
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal128.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal128.java
new file mode 100644
index 0000000..ea7aacf
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal128.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import java.lang.annotation.Native;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+
+public final class Decimal128 extends Number {
+
+    private static final long serialVersionUID = -4863018398624288737L;
+
+    /**
+     * The number of bits used to represent an {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    @Native public static final int SIZE = 128;
+
+    /**
+     * The number of bytes used to represent a {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    public static final int BYTES = SIZE / Byte.SIZE;
+
+    private final BigDecimal underlying;
+    private final long msb;
+    private final long lsb;
+
+    public Decimal128(BigDecimal underlying) {
+        this.underlying = underlying;
+
+        this.msb = calculateMostSignificantBits(underlying);
+        this.lsb = calculateLeastSignificantBits(underlying);
+    }
+
+    public Decimal128(final long msb, final long lsb) {
+        this.msb = msb;
+        this.lsb = lsb;
+
+        this.underlying = calculateBigDecimal(msb, lsb);
+    }
+
+    public Decimal128(byte[] data) {
+        this(ByteBuffer.wrap(data));
+    }
+
+    private Decimal128(final ByteBuffer buffer) {
+        this(buffer.getLong(), buffer.getLong());
+    }
+
+    private static long calculateMostSignificantBits(final BigDecimal underlying) {
+        return 0; // TODO.
+    }
+
+    private static long calculateLeastSignificantBits(final BigDecimal underlying) {
+        return 0; // TODO.
+    }
+
+    private static BigDecimal calculateBigDecimal(final long msb, final long lsb) {
+        return BigDecimal.ZERO; // TODO.
+    }
+
+    @Override
+    public int intValue() {
+        return underlying.intValue();
+    }
+
+    @Override
+    public long longValue() {
+        return underlying.longValue();
+    }
+
+    @Override
+    public float floatValue() {
+        return underlying.floatValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return underlying.doubleValue();
+    }
+
+    public long getMostSignificantBits() {
+        return msb;
+    }
+
+    public long getLeastSignificantBits() {
+        return lsb;
+    }
+
+    public byte[] asBytes() {
+        byte[] bytes = new byte[16];
+        ByteBuffer buf = ByteBuffer.wrap(bytes);
+        buf.putLong(getMostSignificantBits());
+        buf.putLong(getLeastSignificantBits());
+        return bytes;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Decimal128 that = (Decimal128) o;
+
+        if (lsb != that.lsb) {
+            return false;
+        }
+        if (msb != that.msb) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (msb ^ (msb >>> 32));
+        result = 31 * result + (int) (lsb ^ (lsb >>> 32));
+        return result;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal32.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal32.java
new file mode 100644
index 0000000..f721c5b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal32.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import java.lang.annotation.Native;
+import java.math.BigDecimal;
+
+public final class Decimal32 extends Number {
+
+    private static final long serialVersionUID = 1404882516677613318L;
+
+    /**
+     * The number of bits used to represent an {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    @Native public static final int SIZE = 32;
+
+    /**
+     * The number of bytes used to represent a {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    public static final int BYTES = SIZE / Byte.SIZE;
+
+    private final BigDecimal underlying;
+    private final int bits;
+
+    public Decimal32(BigDecimal underlying) {
+        this.underlying = underlying;
+        this.bits = calculateBits(underlying);
+    }
+
+    public Decimal32(final int bits) {
+        this.bits = bits;
+        this.underlying = calculateBigDecimal(bits);
+    }
+
+    static int calculateBits(final BigDecimal underlying) {
+        return 0; // TODO.
+    }
+
+    static BigDecimal calculateBigDecimal(int bits) {
+        return BigDecimal.ZERO; // TODO
+    }
+
+    @Override
+    public int intValue() {
+        return underlying.intValue();
+    }
+
+    @Override
+    public long longValue() {
+        return underlying.longValue();
+    }
+
+    @Override
+    public float floatValue() {
+        return underlying.floatValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return underlying.doubleValue();
+    }
+
+    public int getBits() {
+        return bits;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Decimal32 decimal32 = (Decimal32) o;
+
+        if (bits != decimal32.bits) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return bits;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal64.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal64.java
new file mode 100644
index 0000000..00e66a0
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Decimal64.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import java.lang.annotation.Native;
+import java.math.BigDecimal;
+
+public final class Decimal64 extends Number {
+
+    private static final long serialVersionUID = -3811100445576755687L;
+
+    /**
+     * The number of bits used to represent an {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    @Native public static final int SIZE = 64;
+
+    /**
+     * The number of bytes used to represent a {@code Decimal128} value in two's
+     * complement binary form.
+     */
+    public static final int BYTES = SIZE / Byte.SIZE;
+
+    private final BigDecimal underlying;
+    private final long bits;
+
+    public Decimal64(BigDecimal underlying) {
+        this.underlying = underlying;
+        this.bits = calculateBits(underlying);
+    }
+
+    public Decimal64(final long bits) {
+        this.bits = bits;
+        this.underlying = calculateBigDecimal(bits);
+    }
+
+    static BigDecimal calculateBigDecimal(final long bits) {
+        return BigDecimal.ZERO;
+    }
+
+    static long calculateBits(final BigDecimal underlying) {
+        return 0l; // TODO
+    }
+
+    @Override
+    public int intValue() {
+        return underlying.intValue();
+    }
+
+    @Override
+    public long longValue() {
+        return underlying.longValue();
+    }
+
+    @Override
+    public float floatValue() {
+        return underlying.floatValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return underlying.doubleValue();
+    }
+
+    public long getBits() {
+        return bits;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Decimal64 decimal64 = (Decimal64) o;
+
+        if (bits != decimal64.bits) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (bits ^ (bits >>> 32));
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/DeliveryTag.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/DeliveryTag.java
new file mode 100644
index 0000000..0822227
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/DeliveryTag.java
@@ -0,0 +1,177 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+/**
+ * An abstraction around Transfer frames Binary delivery tag which can be used to
+ * provide additional features to code sending transfers such as tag pooling etc.
+ *
+ * @see ProtonBuffer
+ * @see Transfer
+ */
+public interface DeliveryTag {
+
+    /**
+     * @return the total number of bytes needed to represent the given tag.
+     */
+    int tagLength();
+
+    /**
+     * Returns a view of this {@link DeliveryTag} object as a byte array.  The returned array may
+     * be the actual underlying tag bytes or a synthetic view based on the value used to generate
+     * the tag.  It is advised not to modify the returned value and copy if such modification are
+     * necessary to the caller.
+     *
+     * @return the underlying tag bytes as a byte array that may or may no be a singleton instance..
+     */
+    byte[] tagBytes();
+
+    /**
+     * Returns a view of this {@link DeliveryTag} object as a {@link ProtonBuffer}.  The returned array
+     * may be the actual underlying tag bytes or a synthetic view based on the value used to generate
+     * the tag.  It is advised not to modify the returned value and copy if such modification are
+     * necessary to the caller.
+     *
+     * @return the ProtonBuffer view of the tag bytes.
+     */
+    ProtonBuffer tagBuffer();
+
+    /**
+     * Optional method used by tag implementations that provide pooling of tags.  Implementations can
+     * do nothing here if no release mechanics are needed.
+     */
+    void release();
+
+    /**
+     * Create a copy of this delivery tag, the copy should account for any underlying pooling of tags that
+     * the tag source's implementation is using.
+     *
+     * @return a copy of the underlying bytes that compose this delivery tag.
+     */
+    DeliveryTag copy();
+
+    /**
+     * Writes the tag as a sequence of bytes into the given buffer in the manner most efficient
+     * for the underlying {@link DeliveryTag} implementation.
+     *
+     * @param buffer
+     *      The target buffer where the tag bytes are to be written.
+     */
+    void writeTo(ProtonBuffer buffer);
+
+    /**
+     * A default DeliveryTag implementation that can be used by a codec when decoding DeliveryTag
+     * instances from the wire.
+     */
+    public static class ProtonDeliveryTag implements DeliveryTag {
+
+        public static final ProtonDeliveryTag EMPTY_TAG = new ProtonDeliveryTag();
+
+        private static final byte[] EMPTY_TAG_ARRAY = new byte[0];
+
+        private final byte[] tagBytes;
+        private ProtonBuffer tagView;
+        private Integer hashCode;
+
+        public ProtonDeliveryTag() {
+            this.tagBytes = EMPTY_TAG_ARRAY;
+        }
+
+        public ProtonDeliveryTag(byte[] tagBytes) {
+            Objects.requireNonNull(tagBytes, "Tag bytes cannot be null");
+            this.tagBytes = tagBytes;
+        }
+
+        public ProtonDeliveryTag(ProtonBuffer tagBytes) {
+            Objects.requireNonNull(tagBytes, "Tag bytes cannot be null");
+            if (tagBytes.hasArray() && tagBytes.getArrayOffset() == 0) {
+                this.tagBytes = tagBytes.getArray();
+            } else {
+                this.tagBytes = new byte[tagBytes.getReadableBytes()];
+                tagBytes.getBytes(tagBytes.getReadIndex(), this.tagBytes);
+            }
+            this.tagView = tagBytes;
+        }
+
+        @Override
+        public byte[] tagBytes() {
+            return tagBytes;
+        }
+
+        @Override
+        public int tagLength() {
+            return tagBytes.length;
+        }
+
+        @Override
+        public ProtonBuffer tagBuffer() {
+            if (tagView == null) {
+                tagView = ProtonByteBufferAllocator.DEFAULT.wrap(tagBytes);
+            }
+
+            return tagView;
+        }
+
+        @Override
+        public DeliveryTag copy() {
+            return new ProtonDeliveryTag(Arrays.copyOf(tagBytes, tagBytes.length));
+        }
+
+        @Override
+        public void release() {
+            // Nothing to do for this basic implementation.
+        }
+
+        @Override
+        public int hashCode() {
+            if (hashCode == null) {
+                hashCode = Arrays.hashCode(tagBytes);
+            }
+
+            return hashCode.intValue();
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof DeliveryTag)) {
+                return false;
+            }
+
+            return Arrays.equals(tagBytes, ((DeliveryTag) other).tagBytes());
+        }
+
+        @Override
+        public String toString() {
+            return "DeliveryTag: {" + Arrays.toString(tagBytes) + "}";
+        }
+
+        @Override
+        public void writeTo(ProtonBuffer buffer) {
+            buffer.writeBytes(tagBytes);
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/DescribedType.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/DescribedType.java
new file mode 100644
index 0000000..bc7a0ca
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/DescribedType.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+public interface DescribedType {
+
+    /**
+     * Returns the Described Type descriptor that identified this instance.
+     *
+     * @return the descriptor that identifies this instance.
+     */
+    public Object getDescriptor();
+
+    /**
+     * Returns the described type value that is carried in this instance.
+     *
+     * @return the value carried inside this described instance.
+     */
+    public Object getDescribed();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/Symbol.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Symbol.java
new file mode 100644
index 0000000..b4c604a
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/Symbol.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+
+public final class Symbol implements Comparable<Symbol> {
+
+    private static final Map<ProtonBuffer, Symbol> bufferToSymbols = new ConcurrentHashMap<>(2048);
+    private static final Map<String, Symbol> stringToSymbols = new ConcurrentHashMap<>(2048);
+
+    private static final Symbol EMPTY_SYMBOL = new Symbol();
+
+    private static final int MAX_CACHED_SYMBOL_SIZE = 64;
+
+    private String symbolString;
+    private final ProtonBuffer underlying;
+    private final int hashCode;
+
+    private Symbol() {
+        this.underlying = ProtonByteBufferAllocator.DEFAULT.allocate(0, 0);
+        this.hashCode = 31;
+        this.symbolString = "";
+    }
+
+    private Symbol(ProtonBuffer underlying) {
+        this.underlying = underlying;
+        this.hashCode = underlying.hashCode();
+    }
+
+    public int getLength() {
+        return underlying.getReadableBytes();
+    }
+
+    @Override
+    public int compareTo(Symbol other) {
+        return underlying.compareTo(other.underlying);
+    }
+
+    @Override
+    public String toString() {
+        if (symbolString == null && underlying.getReadableBytes() > 0) {
+            symbolString = underlying.toString(US_ASCII);
+
+            if (underlying.getReadableBytes() <= MAX_CACHED_SYMBOL_SIZE) {
+                final Symbol existing;
+                if ((existing = stringToSymbols.putIfAbsent(symbolString, this)) != null) {
+                    symbolString = existing.symbolString;
+                }
+            }
+        }
+
+        return symbolString;
+    }
+
+    @Override
+    public int hashCode() {
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof Symbol) {
+            return underlying.equals(((Symbol) other).underlying);
+        }
+
+        return false;
+    }
+
+    public void writeTo(ProtonBuffer target) {
+        target.writeBytes(underlying, 0, underlying.getReadableBytes());
+    }
+
+    public static Symbol valueOf(String symbolVal) {
+        return getSymbol(symbolVal);
+    }
+
+    public static Symbol getSymbol(ProtonBuffer symbolBytes) {
+        return getSymbol(symbolBytes, false);
+    }
+
+    public static Symbol getSymbol(ProtonBuffer symbolBuffer, boolean copyOnCreate) {
+        if (symbolBuffer == null) {
+            return null;
+        } else if (symbolBuffer.getReadableBytes() == 0) {
+            return EMPTY_SYMBOL;
+        }
+
+        Symbol symbol = bufferToSymbols.get(symbolBuffer);
+        if (symbol == null) {
+            if (copyOnCreate) {
+                // Copy to a known heap based buffer to avoid issue with life-cycle of pooled buffer types.
+                int symbolSize = symbolBuffer.getReadableBytes();
+                ProtonBuffer copy = ProtonByteBufferAllocator.DEFAULT.allocate(symbolSize, symbolSize);
+                symbolBuffer = copy.setBytes(0, symbolBuffer, 0, symbolSize).setWriteIndex(symbolSize);
+            }
+
+            symbol = new Symbol(symbolBuffer);
+
+            // Don't cache overly large symbols to prevent holding large
+            // amount of memory in the symbol cache.
+            if (symbolBuffer.getReadableBytes() <= MAX_CACHED_SYMBOL_SIZE) {
+                final Symbol existing;
+                if ((existing = bufferToSymbols.putIfAbsent(symbolBuffer, symbol)) != null) {
+                    symbol = existing;
+                }
+            }
+        }
+
+        return symbol;
+    }
+
+    public static Symbol getSymbol(String stringValue) {
+        if (stringValue == null) {
+            return null;
+        } else if (stringValue.isEmpty()) {
+            return EMPTY_SYMBOL;
+        }
+
+        Symbol symbol = stringToSymbols.get(stringValue);
+        if (symbol == null) {
+            symbol = getSymbol(ProtonByteBufferAllocator.DEFAULT.wrap(stringValue.getBytes(US_ASCII)));
+
+            // Don't cache overly large symbols to prevent holding large
+            // amount of memory in the symbol cache.
+            if (symbol.underlying.getReadableBytes() <= MAX_CACHED_SYMBOL_SIZE) {
+                stringToSymbols.put(stringValue, symbol);
+            }
+        }
+
+        return symbol;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnknownDescribedType.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnknownDescribedType.java
new file mode 100644
index 0000000..89a15a8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnknownDescribedType.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+public class UnknownDescribedType implements DescribedType {
+
+    private final Object descriptor;
+    private final Object described;
+
+    public UnknownDescribedType(final Object descriptor, final Object described) {
+        this.descriptor = descriptor;
+        this.described = described;
+    }
+
+    @Override
+    public Object getDescriptor() {
+        return descriptor;
+    }
+
+    @Override
+    public Object getDescribed() {
+        return described;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final UnknownDescribedType that = (UnknownDescribedType) o;
+
+        if (described != null ? !described.equals(that.described) : that.described != null) {
+            return false;
+        }
+        if (descriptor != null ? !descriptor.equals(that.descriptor) : that.descriptor != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = descriptor != null ? descriptor.hashCode() : 0;
+        result = 31 * result + (described != null ? described.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "UnknownDescribedType{" + "descriptor=" + descriptor + ", described=" + described + '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedByte.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedByte.java
new file mode 100644
index 0000000..bd3dbd4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedByte.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+public final class UnsignedByte extends Number implements Comparable<UnsignedByte> {
+
+    private static final long serialVersionUID = 6271683731751283403L;
+    private static final UnsignedByte[] cachedValues = new UnsignedByte[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedByte((byte) i);
+        }
+    }
+
+    private final byte underlying;
+
+    public UnsignedByte(byte underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public byte byteValue() {
+        return underlying;
+    }
+
+    @Override
+    public short shortValue() {
+        return (short) intValue();
+    }
+
+    @Override
+    public int intValue() {
+        return (underlying) & 0xFF;
+    }
+
+    @Override
+    public long longValue() {
+        return (underlying) & 0xFFl;
+    }
+
+    @Override
+    public float floatValue() {
+        return longValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return longValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedByte that = (UnsignedByte) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give byte value to this unsigned byte numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the byte to compare to this unsigned byte instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(byte value) {
+        return compare(underlying, value);
+    }
+
+    @Override
+    public int compareTo(UnsignedByte o) {
+        return compare(underlying, o.underlying);
+    }
+
+    /**
+     * Compares two short values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side short to compare
+     * @param right
+     *       the right hand side short to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(byte left, byte right) {
+        return Integer.compareUnsigned(Byte.toUnsignedInt(left), Byte.toUnsignedInt(right));
+    }
+
+    @Override
+    public int hashCode() {
+        return underlying;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(intValue());
+    }
+
+    /**
+     * Returns an UnsignedByte instance representing the specified byte value. This method always returns
+     * a cached {@link UnsignedByte} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedByte#UnsignedByte(byte)} which will always create a new
+     * instance of the {@link UnsignedByte} type.
+     *
+     * @param value
+     *      The byte value to return as an {@link UnsignedByte} instance.
+     *
+     * @return an {@link UnsignedByte} instance representing the unsigned view of the given byte.
+     */
+    public static UnsignedByte valueOf(byte value) {
+        final int index = (value) & 0xFF;
+        return cachedValues[index];
+    }
+
+    /**
+     * Returns an {@link UnsignedByte} instance representing the specified {@link String} value. This method always
+     * returns a cached {@link UnsignedByte} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedByte#UnsignedByte(byte)} which will always create a new instance
+     * of the {@link UnsignedByte} type.
+     *
+     * @param value
+     *      The byte value to return as an {@link UnsignedByte} instance.
+     *
+     * @return an {@link UnsignedByte} instance representing the unsigned view of the given byte.
+     *
+     * @throws NumberFormatException if the given {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedByte valueOf(final String value) throws NumberFormatException {
+        int intVal = Integer.parseInt(value);
+        if (intVal < 0 || intVal >= (1 << 8)) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [" + 0 + "-" + (1 << 8) + ").");
+        }
+        return valueOf((byte) intVal);
+    }
+
+    /**
+     * Returns a {@code short} that represents the unsigned view of the given {@code byte} value.
+     *
+     * @param value
+     *      The {@code short} whose unsigned value should be converted to a long.
+     *
+     * @return a positive {@code short} value that represents the given {@code byte} as unsigned.
+     */
+    public static short toUnsignedShort(byte value) {
+        return (short) (value & 0xff);
+    }
+
+    /**
+     * Returns a {@code int} that represents the unsigned view of the given {@code byte} value.
+     *
+     * @param value
+     *      The {@code int} whose unsigned value should be converted to a long.
+     *
+     * @return a positive {@code int} value that represents the given {@code short} as unsigned.
+     */
+    public static int toUnsignedInt(byte value) {
+        return Byte.toUnsignedInt(value);
+    }
+
+    /**
+     * Returns a {@code long} that represents the unsigned view of the given {@code byte} value.
+     *
+     * @param value
+     *      The {@code long} whose unsigned value should be converted to a long.
+     *
+     * @return a positive {@code long} value that represents the given {@code byte} as unsigned.
+     */
+    public static long toUnsignedLong(byte value) {
+        return Byte.toUnsignedLong(value);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedInteger.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedInteger.java
new file mode 100644
index 0000000..6fcfe51
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedInteger.java
@@ -0,0 +1,246 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+public final class UnsignedInteger extends Number implements Comparable<UnsignedInteger> {
+
+    private static final long serialVersionUID = 3042749852724499995L;
+    private static final UnsignedInteger[] cachedValues = new UnsignedInteger[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedInteger(i);
+        }
+    }
+
+    public static final UnsignedInteger ZERO = cachedValues[0];
+    public static final UnsignedInteger ONE = cachedValues[1];
+    public static final UnsignedInteger MAX_VALUE = new UnsignedInteger(0xffffffff);
+
+    private final int underlying;
+
+    public UnsignedInteger(int underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public int intValue() {
+        return underlying;
+    }
+
+    @Override
+    public long longValue() {
+        return Integer.toUnsignedLong(underlying);
+    }
+
+    @Override
+    public float floatValue() {
+        return longValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return longValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedInteger that = (UnsignedInteger) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give integer value to this unsigned integer numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the integer to compare to this unsigned integer instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(int value) {
+        return Integer.compareUnsigned(underlying, value);
+    }
+
+    /**
+     * Compares the give long value to this unsigned integer numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the long to compare to this unsigned integer instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(long value) {
+        return Long.compareUnsigned(longValue(), value);
+    }
+
+    @Override
+    public int compareTo(UnsignedInteger value) {
+        return Long.compareUnsigned(longValue(), value.longValue());
+    }
+
+    /**
+     * Compares two integer values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side integer to compare
+     * @param right
+     *       the right hand side integer to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(int left, int right) {
+        return Integer.compareUnsigned(left, right);
+    }
+
+    /**
+     * Compares two long values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side long value to compare
+     * @param right
+     *       the right hand side long value to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(long left, long right) {
+        return Long.compareUnsigned(left, right);
+    }
+
+    @Override
+    public int hashCode() {
+        return underlying;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(longValue());
+    }
+
+    /**
+     * Returns an UnsignedInteger instance representing the specified int value. This method always returns
+     * a cached {@link UnsignedInteger} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedInteger#UnsignedInteger(int)} which will always create a new
+     * instance of the {@link UnsignedInteger} type.
+     *
+     * @param value
+     *      The int value to return as an {@link UnsignedInteger} instance.
+     *
+     * @return an {@link UnsignedInteger} instance representing the unsigned view of the given int.
+     */
+    public static UnsignedInteger valueOf(int value) {
+        if ((value & 0xFFFFFF00) == 0) {
+            return cachedValues[value];
+        } else {
+            return new UnsignedInteger(value);
+        }
+    }
+
+    /**
+     * Adds the value of the given {@link UnsignedInteger} to this instance and return a new {@link UnsignedInteger}
+     * instance that represents the newly computed value.
+     *
+     * @param value
+     *      The {@link UnsignedInteger} whose underlying value should be added to this instance's value.
+     *
+     * @return a new immutable {@link UnsignedInteger} resulting from the addition of this with the given value.
+     */
+    public UnsignedInteger add(final UnsignedInteger value) {
+        int val = underlying + value.underlying;
+        return UnsignedInteger.valueOf(val);
+    }
+
+    /**
+     * Subtract the value of the given {@link UnsignedInteger} from this instance and return a new {@link UnsignedInteger}
+     * instance that represents the newly computed value.
+     *
+     * @param value
+     *      The {@link UnsignedInteger} whose underlying value should be subtracted to this instance's value.
+     *
+     * @return a new immutable {@link UnsignedInteger} resulting from the subtraction the given value from this.
+     */
+    public UnsignedInteger subtract(final UnsignedInteger value) {
+        int val = underlying - value.underlying;
+        return UnsignedInteger.valueOf(val);
+    }
+
+    /**
+     * Returns an {@link UnsignedInteger} instance representing the specified {@link String} value. This method
+     * always returns a cached {@link UnsignedInteger} instance for values in the range [0...255] which can save
+     * space and time over calling the constructor {@link UnsignedInteger#UnsignedInteger(int)} which will always
+     * create a new instance of the {@link UnsignedInteger} type.
+     *
+     * @param value
+     *      The String value to return as an {@link UnsignedInteger} instance.
+     *
+     * @return an {@link UnsignedInteger} instance representing the unsigned view of the given String.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedInteger} value possible
+     *                               or the {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedInteger valueOf(final String value) {
+        long longVal = Long.parseLong(value);
+        return valueOf(longVal);
+    }
+
+    /**
+     * Returns an UnsignedInteger instance representing the specified long value. This method always returns
+     * a cached {@link UnsignedInteger} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedInteger#UnsignedInteger(int)} which will always create a new
+     * instance of the {@link UnsignedInteger} type.
+     *
+     * @param value
+     *      The long value to return as an {@link UnsignedInteger} instance.
+     *
+     * @return an {@link UnsignedInteger} instance representing the unsigned view of the given long.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedInteger} value possible.
+     */
+    public static UnsignedInteger valueOf(final long value) {
+        if (value < 0L || value >= (1L << 32)) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [" + 0L + "-" + (1L << 32) + ").");
+        }
+        return valueOf((int) value);
+    }
+
+    /**
+     * Returns a {@code long} that represents the unsigned view of the given {@code int} value.
+     *
+     * @param value
+     *      The integer whose unsigned value should be converted to a long.
+     *
+     * @return a positive long value that represents the given {@code int} as unsigned.
+     */
+    public static long toUnsignedLong(int value) {
+        return Integer.toUnsignedLong(value);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedLong.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedLong.java
new file mode 100644
index 0000000..d38d8af
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedLong.java
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import java.math.BigInteger;
+
+public final class UnsignedLong extends Number implements Comparable<UnsignedLong> {
+
+    private static final long serialVersionUID = -5901821450224443596L;
+    private static final BigInteger TWO_TO_THE_SIXTY_FOUR = new BigInteger(
+        new byte[] { (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+    private static final BigInteger LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
+
+    private static final UnsignedLong[] cachedValues = new UnsignedLong[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedLong(i);
+        }
+    }
+
+    public static final UnsignedLong ZERO = cachedValues[0];
+    public static final UnsignedLong ONE = cachedValues[1];
+    public static final UnsignedLong MAX_VALUE = new UnsignedLong(0xffffffffffffffffl);
+
+    private final long underlying;
+
+    public UnsignedLong(long underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public int intValue() {
+        return (int) underlying;
+    }
+
+    @Override
+    public long longValue() {
+        return underlying;
+    }
+
+    public BigInteger bigIntegerValue() {
+        if (underlying >= 0L) {
+            return BigInteger.valueOf(underlying);
+        } else {
+            return TWO_TO_THE_SIXTY_FOUR.add(BigInteger.valueOf(underlying));
+        }
+    }
+
+    @Override
+    public float floatValue() {
+        return longValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return longValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedLong that = (UnsignedLong) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give long value to this {@link UnsignedLong} numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the long to compare to this {@link UnsignedLong} instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(long value) {
+        return Long.compareUnsigned(underlying, value);
+    }
+
+    @Override
+    public int compareTo(UnsignedLong o) {
+        return bigIntegerValue().compareTo(o.bigIntegerValue());
+    }
+
+    /**
+     * Compares two long values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side long to compare
+     * @param right
+     *       the right hand side long to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(long left, long right) {
+        return Long.compareUnsigned(left, right);
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (underlying ^ (underlying >>> 32));
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(bigIntegerValue());
+    }
+
+    /**
+     * Returns an UnsignedLong instance representing the specified int value. This method always returns
+     * a cached {@link UnsignedLong} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedLong#UnsignedLong(long)} which will always create a new
+     * instance of the {@link UnsignedLong} type.
+     *
+     * @param value
+     *      The long value to return as an {@link UnsignedLong} instance.
+     *
+     * @return an {@link UnsignedLong} instance representing the unsigned view of the given long.
+     */
+    public static UnsignedLong valueOf(long value) {
+        if ((value & 0xFFL) == value) {
+            return cachedValues[(int) value];
+        } else {
+            return new UnsignedLong(value);
+        }
+    }
+
+    /**
+     * Returns an {@link UnsignedLong} instance representing the specified {@link String} value. This method
+     * always returns a cached {@link UnsignedLong} instance for values in the range [0...255] which can save
+     * space and time over calling the constructor {@link UnsignedLong#UnsignedLong(long)} which will always
+     * create a new instance of the {@link UnsignedLong} type.
+     *
+     * @param value
+     *      The String value to return as an {@link UnsignedLong} instance.
+     *
+     * @return an {@link UnsignedLong} instance representing the unsigned view of the given String.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedLong} value possible
+     *                               or the {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedLong valueOf(final String value) {
+        BigInteger bigInt = new BigInteger(value);
+
+        return valueOf(bigInt);
+    }
+
+    /**
+     * Returns an {@link UnsignedLong} instance representing the specified {@link BigInteger} value. This method
+     * always returns a cached {@link UnsignedLong} instance for values in the range [0...255] which can save
+     * space and time over calling the constructor {@link UnsignedLong#UnsignedLong(long)} which will always
+     * create a new instance of the {@link UnsignedLong} type.
+     *
+     * @param value
+     *      The {@link BigInteger} value to return as an {@link UnsignedLong} instance.
+     *
+     * @return an {@link UnsignedLong} instance representing the unsigned view of the given {@link BigInteger}.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedLong} value possible.
+     */
+    public static UnsignedLong valueOf(BigInteger value) {
+        if (value.signum() == -1 || value.bitLength() > 64) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [0 - 2^64).");
+        } else if (value.compareTo(LONG_MAX_VALUE) >= 0) {
+            return UnsignedLong.valueOf(value.longValue());
+        } else {
+            return UnsignedLong.valueOf(TWO_TO_THE_SIXTY_FOUR.subtract(value).negate().longValue());
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedShort.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedShort.java
new file mode 100644
index 0000000..cb27b50
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/UnsignedShort.java
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+public final class UnsignedShort extends Number implements Comparable<UnsignedShort> {
+
+    private static final long serialVersionUID = 6006944990203315231L;
+    private static final UnsignedShort[] cachedValues = new UnsignedShort[256];
+
+    static {
+        for (short i = 0; i < 256; i++) {
+            cachedValues[i] = new UnsignedShort(i);
+        }
+    }
+
+    public static final UnsignedShort MAX_VALUE = new UnsignedShort((short) -1);
+
+    private final short underlying;
+
+    public UnsignedShort(short underlying) {
+        this.underlying = underlying;
+    }
+
+    @Override
+    public short shortValue() {
+        return underlying;
+    }
+
+    @Override
+    public int intValue() {
+        return Short.toUnsignedInt(underlying);
+    }
+
+    @Override
+    public long longValue() {
+        return Short.toUnsignedLong(underlying);
+    }
+
+    @Override
+    public float floatValue() {
+        return intValue();
+    }
+
+    @Override
+    public double doubleValue() {
+        return intValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UnsignedShort that = (UnsignedShort) o;
+
+        if (underlying != that.underlying) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares the give short value to this unsigned short numerically treating the given value as unsigned.
+     *
+     * @param value
+     *       the short to compare to this unsigned short instance.
+     *
+     * @return the value 0 if this == value; a value less than 0 if this &lt; value as unsigned values; and a value
+     *         greater than 0 if this &gt; value as unsigned values
+     */
+    public int compareTo(short value) {
+        return Integer.signum(intValue() - Short.toUnsignedInt(value));
+    }
+
+    @Override
+    public int compareTo(UnsignedShort value) {
+        return Integer.signum(intValue() - value.intValue());
+    }
+
+    /**
+     * Compares two short values numerically treating the values as unsigned.
+     *
+     * @param left
+     *       the left hand side short to compare
+     * @param right
+     *       the right hand side short to compare
+     *
+     * @return the value 0 if left == right; a value less than 0 if left &lt; right as unsigned values; and a value
+     *         greater than 0 if left &gt; right as unsigned values
+     */
+    public static int compare(short left, short right) {
+        return Integer.compareUnsigned(Short.toUnsignedInt(left), Short.toUnsignedInt(right));
+    }
+
+    @Override
+    public int hashCode() {
+        return underlying;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(longValue());
+    }
+
+    /**
+     * Returns an UnsignedShort instance representing the specified short value. This method always returns
+     * a cached {@link UnsignedShort} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedShort#UnsignedShort(short)} which will always create a new
+     * instance of the {@link UnsignedShort} type.
+     *
+     * @param value
+     *      The short value to return as an {@link UnsignedShort} instance.
+     *
+     * @return an {@link UnsignedShort} instance representing the unsigned view of the given short.
+     */
+    public static UnsignedShort valueOf(final short value) {
+        if ((value & 0xFF00) == 0) {
+            return cachedValues[value];
+        } else {
+            return new UnsignedShort(value);
+        }
+    }
+
+    /**
+     * Returns an UnsignedShort instance representing the specified int value. This method always returns
+     * a cached {@link UnsignedShort} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedShort#UnsignedShort(short)} which will always create a new
+     * instance of the {@link UnsignedShort} type.
+     *
+     * @param value
+     *      The short value to return as an {@link UnsignedShort} instance.
+     *
+     * @return an {@link UnsignedShort} instance representing the unsigned view of the given short.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedShort} value possible.
+     */
+    public static UnsignedShort valueOf(final int value) {
+        if (value < 0L || value >= (1L << 16)) {
+            throw new NumberFormatException("Value \"" + value + "\" lies outside the range [" + 0L + "-" + (1L << 16) + ").");
+        }
+        return valueOf((short) value);
+    }
+
+    /**
+     * Returns an UnsignedShort instance representing the specified {@link String} value. This method always returns
+     * a cached {@link UnsignedShort} instance for values in the range [0...255] which can save space and time
+     * over calling the constructor {@link UnsignedShort#UnsignedShort(short)} which will always create a new
+     * instance of the {@link UnsignedShort} type.
+     *
+     * @param value
+     *      The String value to return as an {@link UnsignedShort} instance.
+     *
+     * @return an {@link UnsignedShort} instance representing the unsigned view of the given String.
+     *
+     * @throws NumberFormatException if the given value is greater than the max {@link UnsignedShort} value possible
+     *                               or the {@link String} value given cannot be converted to a numeric value.
+     */
+    public static UnsignedShort valueOf(final String value) {
+        int intVal = Integer.parseInt(value);
+
+        if (intVal < 0 || intVal >= (1 << 16)) {
+            throw new NumberFormatException(
+                "Value \"" + value + "\" lies outside the range [" + 0 + "-" + (1 << 16) + ").");
+        }
+
+        return valueOf((short) intVal);
+    }
+
+    /**
+     * Returns a {@code int} that represents the unsigned view of the given {@code short} value.
+     *
+     * @param value
+     *      The integer whose unsigned value should be converted to a long.
+     *
+     * @return a positive long value that represents the given {@code short} as unsigned.
+     */
+    public static int toUnsignedInt(short value) {
+        return Short.toUnsignedInt(value);
+    }
+
+    /**
+     * Returns a {@code long} that represents the unsigned view of the given {@code short} value.
+     *
+     * @param value
+     *      The integer whose unsigned value should be converted to a long.
+     *
+     * @return a positive long value that represents the given {@code short} as unsigned.
+     */
+    public static long toUnsignedLong(short value) {
+        return Short.toUnsignedLong(value);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Accepted.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Accepted.java
new file mode 100644
index 0000000..c81cc95
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Accepted.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+public final class Accepted implements DeliveryState, Outcome {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000024L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:accepted:list");
+
+    private static final Accepted INSTANCE = new Accepted();
+
+    private Accepted() {
+    }
+
+    @Override
+    public String toString() {
+        return "Accepted{}";
+    }
+
+    public static Accepted getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Accepted;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/AmqpSequence.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/AmqpSequence.java
new file mode 100644
index 0000000..36ee0ed
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/AmqpSequence.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class AmqpSequence<E> implements Section<List<E>> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000076L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:amqp-sequence:list");
+
+    private final List<E> value;
+
+    public AmqpSequence(List<E> value) {
+        this.value = value;
+    }
+
+    @Override
+    public List<E> getValue() {
+        return value;
+    }
+
+    public AmqpSequence<E> copy() {
+        return new AmqpSequence<>(value != null ? new ArrayList<>(value) : null);
+    }
+
+    @Override
+    public String toString() {
+        return "AmqpSequence{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.AmqpSequence;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        AmqpSequence<?> other = (AmqpSequence<?>) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/AmqpValue.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/AmqpValue.java
new file mode 100644
index 0000000..d2bd26d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/AmqpValue.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class AmqpValue<E> implements Section<E> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000077L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:amqp-value:*");
+
+    private final E value;
+
+    public AmqpValue(E value) {
+        this.value = value;
+    }
+
+    public AmqpValue<E> copy() {
+        return new AmqpValue<>(value);
+    }
+
+    @Override
+    public E getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "AmqpValue{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.AmqpValue;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        AmqpValue<?> other = (AmqpValue<?>) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/ApplicationProperties.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/ApplicationProperties.java
new file mode 100644
index 0000000..d3e408d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/ApplicationProperties.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class ApplicationProperties implements Section<Map<String, Object>> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000074L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:application-properties:map");
+
+    private final Map<String, Object> value;
+
+    @SuppressWarnings("unchecked")
+    public ApplicationProperties(Map<String, ?> value) {
+        this.value = (Map<String, Object>) value;
+    }
+
+    public ApplicationProperties copy() {
+        return new ApplicationProperties(value == null ? null : new LinkedHashMap<>(value));
+    }
+
+    @Override
+    public Map<String, Object> getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "ApplicationProperties{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.ApplicationProperties;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        ApplicationProperties other = (ApplicationProperties) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Data.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Data.java
new file mode 100644
index 0000000..ff872ba
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Data.java
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Data implements Section<byte[]> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000075L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:data:binary");
+
+    private final Binary value;
+
+    public Data(Binary value) {
+        this.value = value;
+    }
+
+    public Data(ProtonBuffer value) {
+        this.value = value != null ? new Binary(value) : null;
+    }
+
+    public Data(byte[] value) {
+        this.value = value != null ? new Binary(value) : null;
+    }
+
+    public Data(byte[] value, int offset, int length) {
+        this.value = value != null ? new Binary(value, offset, length) : null;
+    }
+
+    public Data copy() {
+        return new Data(value == null ? null : value.copy());
+    }
+
+    public Binary getBinary() {
+        return value;
+    }
+
+    /**
+     * Returns the backing array for this Data {@link Section} copying the contents into a new array
+     * instance if the backing array in the contained Binary is a subsequence of a larger referenced
+     * array instance.
+     *
+     * @return the byte array view of this Data {@link Section} {@link Binary} payload.
+     */
+    @Override
+    public byte[] getValue() {
+        if (value != null && value.hasArray() && value.getArrayOffset() == 0 && value.getLength() == value.getArray().length) {
+            return value.getArray();
+        } else {
+            return value != null ? value.arrayCopy() : null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Data{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.Data;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        Data other = (Data) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnClose.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnClose.java
new file mode 100644
index 0000000..c7f169c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnClose.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class DeleteOnClose implements LifetimePolicy {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002bL);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-close:list");
+
+    private static final DeleteOnClose INSTANCE = new DeleteOnClose();
+
+    private DeleteOnClose() {
+    }
+
+    @Override
+    public String toString() {
+        return "DeleteOnClose{}";
+    }
+
+    public static DeleteOnClose getInstance() {
+        return INSTANCE;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinks.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinks.java
new file mode 100644
index 0000000..6db70e3
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinks.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class DeleteOnNoLinks implements LifetimePolicy {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002cL);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-no-links:list");
+
+    private static final DeleteOnNoLinks INSTANCE = new DeleteOnNoLinks();
+
+    private DeleteOnNoLinks() {
+    }
+
+    @Override
+    public String toString() {
+        return "DeleteOnNoLinks{}";
+    }
+
+    public static DeleteOnNoLinks getInstance() {
+        return INSTANCE;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksOrMessages.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksOrMessages.java
new file mode 100644
index 0000000..979ed91
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksOrMessages.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class DeleteOnNoLinksOrMessages implements LifetimePolicy {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002eL);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-no-links-or-messages:list");
+
+    private static final DeleteOnNoLinksOrMessages INSTANCE = new DeleteOnNoLinksOrMessages();
+
+    private DeleteOnNoLinksOrMessages() {
+    }
+
+    @Override
+    public String toString() {
+        return "DeleteOnNoLinksOrMessages{}";
+    }
+
+    public static DeleteOnNoLinksOrMessages getInstance() {
+        return INSTANCE;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoMessages.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoMessages.java
new file mode 100644
index 0000000..a4df50b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoMessages.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class DeleteOnNoMessages implements LifetimePolicy {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000002dL);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delete-on-no-messages:list");
+
+    private static final DeleteOnNoMessages INSTANCE = new DeleteOnNoMessages();
+
+    private DeleteOnNoMessages() {
+    }
+
+    @Override
+    public String toString() {
+        return "DeleteOnNoMessages{}";
+    }
+
+    public static DeleteOnNoMessages getInstance() {
+        return INSTANCE;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeliveryAnnotations.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeliveryAnnotations.java
new file mode 100644
index 0000000..f0d2cd8
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/DeliveryAnnotations.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class DeliveryAnnotations implements Section<Map<Symbol, Object>> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000071L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:delivery-annotations:map");
+
+    private final Map<Symbol, Object> value;
+
+    @SuppressWarnings("unchecked")
+    public DeliveryAnnotations(Map<Symbol, ?> value) {
+        this.value = (Map<Symbol, Object>) value;
+    }
+
+    public DeliveryAnnotations copy() {
+        return new DeliveryAnnotations(value == null ? null : new LinkedHashMap<>(value));
+    }
+
+    @Override
+    public Map<Symbol, Object> getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "DeliveryAnnotations{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.DeliveryAnnotations;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        DeliveryAnnotations other = (DeliveryAnnotations) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Footer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Footer.java
new file mode 100644
index 0000000..ff1c2d9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Footer.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Footer implements Section<Map<Symbol, Object>> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000078L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:footer:map");
+
+    private final Map<Symbol, Object> value;
+
+    @SuppressWarnings("unchecked")
+    public Footer(Map<Symbol, ?> value) {
+        this.value = (Map<Symbol, Object>) value;
+    }
+
+    public Footer copy() {
+        return new Footer(value == null ? null : new LinkedHashMap<>(value));
+    }
+
+    @Override
+    public Map<Symbol, Object> getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Footer{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.Footer;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        Footer other = (Footer) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Header.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Header.java
new file mode 100644
index 0000000..72d5085
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Header.java
@@ -0,0 +1,241 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Header implements Section<Header> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000070L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:header:list");
+
+    public static final boolean DEFAULT_DURABILITY = false;
+    public static final byte DEFAULT_PRIORITY = 4;
+    public static final long DEFAULT_TIME_TO_LIVE = UnsignedInteger.MAX_VALUE.longValue();
+    public static final boolean DEFAULT_FIRST_ACQUIRER = false;
+    public static final long DEFAULT_DELIVERY_COUNT = 0;
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int DURABLE = 1;
+    private static int PRIORITY = 2;
+    private static int TIME_TO_LIVE = 4;
+    private static int FIRST_ACQUIRER = 8;
+    private static int DELIVERY_COUNT = 16;
+
+    private int modified = 0;
+
+    private boolean durable = DEFAULT_DURABILITY;
+    private byte priority = DEFAULT_PRIORITY;
+    private long timeToLive = DEFAULT_TIME_TO_LIVE;
+    private boolean firstAcquirer = DEFAULT_FIRST_ACQUIRER;
+    private long deliveryCount = DEFAULT_DELIVERY_COUNT;
+
+    public Header() {
+    }
+
+    public Header(Header other) {
+        this.durable = other.durable;
+        this.priority = other.priority;
+        this.timeToLive = other.timeToLive;
+        this.firstAcquirer = other.firstAcquirer;
+        this.deliveryCount = other.deliveryCount;
+    }
+
+    public Header copy() {
+        return new Header(this);
+    }
+
+    @Override
+    public Header getValue() {
+        return this;
+    }
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasDurable() {
+        return (modified & DURABLE) == DURABLE;
+    }
+
+    public boolean hasPriority() {
+        return (modified & PRIORITY) == PRIORITY;
+    }
+
+    public boolean hasTimeToLive() {
+        return (modified & TIME_TO_LIVE) == TIME_TO_LIVE;
+    }
+
+    public boolean hasFirstAcquirer() {
+        return (modified & FIRST_ACQUIRER) == FIRST_ACQUIRER;
+    }
+
+    public boolean hasDeliveryCount() {
+        return (modified & DELIVERY_COUNT) == DELIVERY_COUNT;
+    }
+
+    //----- Access the AMQP Header object ------------------------------------//
+
+    public boolean isDurable() {
+        return durable;
+    }
+
+    public Header setDurable(boolean value) {
+        if (value) {
+            modified |= DURABLE;
+        } else {
+            modified &= ~DURABLE;
+        }
+
+        durable = value;
+        return this;
+    }
+
+    public Header clearDurable() {
+        modified &= ~DURABLE;
+        durable = DEFAULT_DURABILITY;
+        return this;
+    }
+
+    public byte getPriority() {
+        return priority;
+    }
+
+    public Header setPriority(byte value) {
+        if (value == DEFAULT_PRIORITY) {
+            modified &= ~PRIORITY;
+        } else {
+            modified |= PRIORITY;
+        }
+
+        priority = value;
+        return this;
+    }
+
+    public Header clearPriority() {
+        modified &= ~PRIORITY;
+        priority = DEFAULT_PRIORITY;
+        return this;
+    }
+
+    public long getTimeToLive() {
+        return timeToLive;
+    }
+
+    public Header setTimeToLive(int value) {
+        return setTimeToLive(Integer.toUnsignedLong(value));
+    }
+
+    public Header setTimeToLive(long value) {
+        if (value < 0 || value > UINT_MAX) {
+            throw new IllegalArgumentException("TTL value given is out of range: " + value);
+        } else {
+            modified |= TIME_TO_LIVE;
+        }
+
+        timeToLive = value;
+        return this;
+    }
+
+    public Header clearTimeToLive() {
+        modified &= ~TIME_TO_LIVE;
+        timeToLive = DEFAULT_TIME_TO_LIVE;
+        return this;
+    }
+
+    public boolean isFirstAcquirer() {
+        return firstAcquirer;
+    }
+
+    public Header setFirstAcquirer(boolean value) {
+        if (value) {
+            modified |= FIRST_ACQUIRER;
+        } else {
+            modified &= ~FIRST_ACQUIRER;
+        }
+
+        firstAcquirer = value;
+        return this;
+    }
+
+    public Header clearFirstAcquirer() {
+        modified &= ~FIRST_ACQUIRER;
+        firstAcquirer = DEFAULT_FIRST_ACQUIRER;
+        return this;
+    }
+
+    public long getDeliveryCount() {
+        return deliveryCount;
+    }
+
+    public Header setDeliveryCount(int value) {
+        return setDeliveryCount(Integer.toUnsignedLong(value));
+    }
+
+    public Header setDeliveryCount(long value) {
+        if (value < 0 || value > UINT_MAX) {
+            throw new IllegalArgumentException("Delivery Count value given is out of range: " + value);
+        } else if (value == 0) {
+            modified &= ~DELIVERY_COUNT;
+        } else {
+            modified |= DELIVERY_COUNT;
+        }
+
+        deliveryCount = value;
+        return this;
+    }
+
+    public Header clearDeliveryCount() {
+        modified &= ~DELIVERY_COUNT;
+        deliveryCount = DEFAULT_DELIVERY_COUNT;
+        return this;
+    }
+
+    public Header reset() {
+        modified = 0;
+        durable = DEFAULT_DURABILITY;
+        priority = DEFAULT_PRIORITY;
+        timeToLive = DEFAULT_TIME_TO_LIVE;
+        firstAcquirer = DEFAULT_FIRST_ACQUIRER;
+        deliveryCount = DEFAULT_DELIVERY_COUNT;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Header{ " +
+                "durable=" + durable +
+                ", priority=" + priority +
+                ", ttl=" + timeToLive +
+                ", firstAcquirer=" + firstAcquirer +
+                ", deliveryCount=" + deliveryCount + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.Header;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/LifetimePolicy.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/LifetimePolicy.java
new file mode 100644
index 0000000..072b5de
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/LifetimePolicy.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+public interface LifetimePolicy {
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/MessageAnnotations.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/MessageAnnotations.java
new file mode 100644
index 0000000..9fddade
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/MessageAnnotations.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class MessageAnnotations implements Section<Map<Symbol, Object>> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000072L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:message-annotations:map");
+
+    private final Map<Symbol, Object> value;
+
+    @SuppressWarnings("unchecked")
+    public MessageAnnotations(Map<Symbol, ?> value) {
+        this.value = (Map<Symbol, Object>) value;
+    }
+
+    public MessageAnnotations copy() {
+        return new MessageAnnotations(value == null ? null : new LinkedHashMap<>(value));
+    }
+
+    @Override
+    public Map<Symbol, Object> getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "MessageAnnotations{ " + value + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.MessageAnnotations;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        MessageAnnotations other = (MessageAnnotations) obj;
+        if (value == null) {
+            if (other.value != null) {
+                return false;
+            }
+        } else if (!value.equals(other.value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Modified.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Modified.java
new file mode 100644
index 0000000..4817043
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Modified.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+public final class Modified implements DeliveryState, Outcome {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000027L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:modified:list");
+
+    private boolean deliveryFailed;
+    private boolean undeliverableHere;
+    private Map<Symbol, Object> messageAnnotations;
+
+    public Modified() {}
+
+    public Modified(boolean deliveryFailed, boolean undeliverableHere) {
+        this(deliveryFailed, undeliverableHere, null);
+    }
+
+    public Modified(boolean deliveryFailed, boolean undeliverableHere, Map<Symbol, Object> annotations) {
+        this.deliveryFailed = deliveryFailed;
+        this.undeliverableHere = undeliverableHere;
+        this.messageAnnotations = annotations != null ? new HashMap<>(annotations) : null;
+    }
+
+    public boolean isDeliveryFailed() {
+        return deliveryFailed;
+    }
+
+    public Modified setDeliveryFailed(boolean deliveryFailed) {
+        this.deliveryFailed = deliveryFailed;
+        return this;
+    }
+
+    public boolean isUndeliverableHere() {
+        return undeliverableHere;
+    }
+
+    public Modified setUndeliverableHere(boolean undeliverableHere) {
+        this.undeliverableHere = undeliverableHere;
+        return this;
+    }
+
+    public Map<Symbol, Object> getMessageAnnotations() {
+        return messageAnnotations;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Modified setMessageAnnotations(Map<Symbol, ?> messageAnnotations) {
+        this.messageAnnotations = (Map<Symbol, Object>) messageAnnotations;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Modified{" +
+               "deliveryFailed=" + deliveryFailed +
+               ", undeliverableHere=" + undeliverableHere +
+               ", messageAnnotations=" + messageAnnotations +
+               '}';
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Modified;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Outcome.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Outcome.java
new file mode 100644
index 0000000..35d5d57
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Outcome.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+public interface Outcome {
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Properties.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Properties.java
new file mode 100644
index 0000000..8f7b10d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Properties.java
@@ -0,0 +1,424 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Properties implements Section<Properties> {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000073L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:properties:list");
+
+    private static int MESSAGE_ID = 1;
+    private static int USER_ID = 2;
+    private static int TO = 4;
+    private static int SUBJECT = 8;
+    private static int REPLY_TO = 16;
+    private static int CORRELATION_ID = 32;
+    private static int CONTENT_TYPE = 64;
+    private static int CONTENT_ENCODING = 128;
+    private static int ABSOLUTE_EXPIRY = 256;
+    private static int CREATION_TIME = 512;
+    private static int GROUP_ID = 1024;
+    private static int GROUP_SEQUENCE = 2048;
+    private static int REPLY_TO_GROUP_ID = 4096;
+
+    private int modified = 0;
+
+    private Object messageId;
+    private Binary userId;
+    private String to;
+    private String subject;
+    private String replyTo;
+    private Object correlationId;
+    private String contentType;
+    private String contentEncoding;
+    private long absoluteExpiryTime;
+    private long creationTime;
+    private String groupId;
+    private long groupSequence;
+    private String replyToGroupId;
+
+    public Properties() {
+    }
+
+    public Properties(Properties other) {
+        this.messageId = other.messageId;
+        this.userId = other.userId;
+        this.to = other.to;
+        this.subject = other.subject;
+        this.replyTo = other.replyTo;
+        this.correlationId = other.correlationId;
+        this.contentType = other.contentType;
+        this.contentEncoding = other.contentEncoding;
+        this.absoluteExpiryTime = other.absoluteExpiryTime;
+        this.creationTime = other.creationTime;
+        this.groupId = other.groupId;
+        this.groupSequence = other.groupSequence;
+        this.replyToGroupId = other.replyToGroupId;
+        this.modified = other.modified;
+    }
+
+    public Properties copy() {
+        return new Properties(this);
+    }
+
+    @Override
+    public Properties getValue() {
+        return this;
+    }
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasMessageId() {
+        return (modified & MESSAGE_ID) == MESSAGE_ID;
+    }
+
+    public boolean hasUserId() {
+        return (modified & USER_ID) == USER_ID;
+    }
+
+    public boolean hasTo() {
+        return (modified & TO) == TO;
+    }
+
+    public boolean hasSubject() {
+        return (modified & SUBJECT) == SUBJECT;
+    }
+
+    public boolean hasReplyTo() {
+        return (modified & REPLY_TO) == REPLY_TO;
+    }
+
+    public boolean hasCorrelationId() {
+        return (modified & CORRELATION_ID) == CORRELATION_ID;
+    }
+
+    public boolean hasContentType() {
+        return (modified & CONTENT_TYPE) == CONTENT_TYPE;
+    }
+
+    public boolean hasContentEncoding() {
+        return (modified & CONTENT_ENCODING) == CONTENT_ENCODING;
+    }
+
+    public boolean hasAbsoluteExpiryTime() {
+        return (modified & ABSOLUTE_EXPIRY) == ABSOLUTE_EXPIRY;
+    }
+
+    public boolean hasCreationTime() {
+        return (modified & CREATION_TIME) == CREATION_TIME;
+    }
+
+    public boolean hasGroupId() {
+        return (modified & GROUP_ID) == GROUP_ID;
+    }
+
+    public boolean hasGroupSequence() {
+        return (modified & GROUP_SEQUENCE) == GROUP_SEQUENCE;
+    }
+
+    public boolean hasReplyToGroupId() {
+        return (modified & REPLY_TO_GROUP_ID) == REPLY_TO_GROUP_ID;
+    }
+
+    //----- Access the AMQP Header object ------------------------------------//
+
+    public Object getMessageId() {
+        return messageId;
+    }
+
+    public Properties setMessageId(Object messageId) {
+        validateIsMessageIdType(messageId);
+
+        if (messageId == null) {
+            modified &= ~MESSAGE_ID;
+        } else {
+            modified |= MESSAGE_ID;
+        }
+
+        this.messageId = messageId;
+
+        return this;
+    }
+
+    private static void validateIsMessageIdType(Object messageId) {
+        if (messageId == null ||
+            messageId instanceof String ||
+            messageId instanceof UUID ||
+            messageId instanceof UnsignedLong ||
+            messageId instanceof Binary) {
+
+            // Allowed types of message.
+            return;
+        }
+
+        throw new IllegalArgumentException(
+            "AMQP Message ID type restiction violated, cannot assign type: " + messageId.getClass().getName());
+    }
+
+    public Binary getUserId() {
+        return userId;
+    }
+
+    public Properties setUserId(byte[] userId) {
+        if (userId == null) {
+            setUserId((Binary) null);
+        } else {
+            setUserId(new Binary(userId));
+        }
+
+        return this;
+    }
+
+    public Properties setUserId(Binary userId) {
+        if (userId == null) {
+            modified &= ~USER_ID;
+        } else {
+            modified |= USER_ID;
+        }
+
+        this.userId = userId;
+        return this;
+    }
+
+    public String getTo() {
+        return to;
+    }
+
+    public Properties setTo(String to) {
+        if (to == null) {
+            modified &= ~TO;
+        } else {
+            modified |= TO;
+        }
+
+        this.to = to;
+        return this;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+
+    public Properties setSubject(String subject) {
+        if (subject == null) {
+            modified &= ~SUBJECT;
+        } else {
+            modified |= SUBJECT;
+        }
+
+        this.subject = subject;
+        return this;
+    }
+
+    public String getReplyTo() {
+        return replyTo;
+    }
+
+    public Properties setReplyTo(String replyTo) {
+        if (replyTo == null) {
+            modified &= ~REPLY_TO;
+        } else {
+            modified |= REPLY_TO;
+        }
+
+        this.replyTo = replyTo;
+        return this;
+    }
+
+    public Object getCorrelationId() {
+        return correlationId;
+    }
+
+    public Properties setCorrelationId(Object correlationId) {
+        validateIsMessageIdType(messageId);
+
+        if (correlationId == null) {
+            modified &= ~CORRELATION_ID;
+        } else {
+            modified |= CORRELATION_ID;
+        }
+
+        this.correlationId = correlationId;
+        return this;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public Properties setContentType(String contentType) {
+        if (contentType == null) {
+            modified &= ~CONTENT_TYPE;
+        } else {
+            modified |= CONTENT_TYPE;
+        }
+
+        this.contentType = contentType;
+        return this;
+    }
+
+    public String getContentEncoding() {
+        return contentEncoding;
+    }
+
+    public Properties setContentEncoding(String contentEncoding) {
+        if (contentEncoding == null) {
+            modified &= ~CONTENT_ENCODING;
+        } else {
+            modified |= CONTENT_ENCODING;
+        }
+
+        this.contentEncoding = contentEncoding;
+        return this;
+    }
+
+    public long getAbsoluteExpiryTime() {
+        return absoluteExpiryTime;
+    }
+
+    public Properties setAbsoluteExpiryTime(int absoluteExpiryTime) {
+        modified |= ABSOLUTE_EXPIRY;
+        this.absoluteExpiryTime = Integer.toUnsignedLong(absoluteExpiryTime);
+        return this;
+    }
+
+    public Properties setAbsoluteExpiryTime(long absoluteExpiryTime) {
+        modified |= ABSOLUTE_EXPIRY;
+        this.absoluteExpiryTime = absoluteExpiryTime;
+        return this;
+    }
+
+    public void clearAbsoluteExpiryTime() {
+        modified &= ~ABSOLUTE_EXPIRY;
+        absoluteExpiryTime = 0;
+    }
+
+    public long getCreationTime() {
+        return creationTime;
+    }
+
+    public Properties setCreationTime(int creationTime) {
+        modified |= CREATION_TIME;
+        this.creationTime = Integer.toUnsignedLong(creationTime);
+        return this;
+    }
+
+    public Properties setCreationTime(long creationTime) {
+        modified |= CREATION_TIME;
+        this.creationTime = creationTime;
+        return this;
+    }
+
+    public void clearCreationTime() {
+        modified &= ~CREATION_TIME;
+        creationTime = 0;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public Properties setGroupId(String groupId) {
+        if (groupId == null) {
+            modified &= ~GROUP_ID;
+        } else {
+            modified |= GROUP_ID;
+        }
+
+        this.groupId = groupId;
+        return this;
+    }
+
+    public long getGroupSequence() {
+        return groupSequence;
+    }
+
+    public Properties setGroupSequence(int groupSequence) {
+        this.modified |= GROUP_SEQUENCE;
+        this.groupSequence = Integer.toUnsignedLong(groupSequence);
+        return this;
+    }
+
+    public Properties setGroupSequence(long groupSequence) {
+        if (groupSequence < 0 || groupSequence > UnsignedInteger.MAX_VALUE.longValue()) {
+            throw new IllegalArgumentException("Group Sequence value given is out of range: " + groupSequence);
+        } else {
+            modified |= GROUP_SEQUENCE;
+        }
+
+        this.groupSequence = groupSequence;
+        return this;
+    }
+
+    public void clearGroupSequence() {
+        modified &= ~GROUP_SEQUENCE;
+        groupSequence = 0l;
+    }
+
+    public String getReplyToGroupId() {
+        return replyToGroupId;
+    }
+
+    public Properties setReplyToGroupId(String replyToGroupId) {
+        if (replyToGroupId == null) {
+            modified &= ~REPLY_TO_GROUP_ID;
+        } else {
+            modified |= REPLY_TO_GROUP_ID;
+        }
+
+        this.replyToGroupId = replyToGroupId;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Properties{" +
+                "messageId=" + messageId +
+                ", userId=" + userId +
+                ", to='" + to + '\'' +
+                ", subject='" + subject + '\'' +
+                ", replyTo='" + replyTo + '\'' +
+                ", correlationId=" + correlationId +
+                ", contentType=" + contentType +
+                ", contentEncoding=" + contentEncoding +
+                ", absoluteExpiryTime=" + absoluteExpiryTime +
+                ", creationTime=" + creationTime +
+                ", groupId='" + groupId + '\'' +
+                ", groupSequence=" + groupSequence +
+                ", replyToGroupId='" + replyToGroupId + '\'' + " }";
+    }
+
+    @Override
+    public SectionType getType() {
+        return SectionType.Properties;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Received.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Received.java
new file mode 100644
index 0000000..9ecc146
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Received.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+public final class Received implements DeliveryState {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000023L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:received:list");
+
+    private UnsignedInteger sectionNumber;
+    private UnsignedLong sectionOffset;
+
+    public UnsignedInteger getSectionNumber() {
+        return sectionNumber;
+    }
+
+    public Received setSectionNumber(UnsignedInteger sectionNumber) {
+        this.sectionNumber = sectionNumber;
+        return this;
+    }
+
+    public UnsignedLong getSectionOffset() {
+        return sectionOffset;
+    }
+
+    public Received setSectionOffset(UnsignedLong sectionOffset) {
+        this.sectionOffset = sectionOffset;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Received{" +
+               "sectionNumber=" + sectionNumber +
+               ", sectionOffset=" + sectionOffset +
+               '}';
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Received;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Rejected.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Rejected.java
new file mode 100644
index 0000000..1957bfe
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Rejected.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+
+public final class Rejected implements DeliveryState, Outcome {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000025L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:rejected:list");
+
+    private ErrorCondition error;
+
+    public Rejected() {}
+
+    public Rejected(ErrorCondition error) {
+        this.error = error;
+    }
+
+    public ErrorCondition getError() {
+        return error;
+    }
+
+    public Rejected setError(ErrorCondition error) {
+        this.error = error;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Rejected{" +
+               "error=" + error + "}";
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Rejected;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Released.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Released.java
new file mode 100644
index 0000000..6339773
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Released.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+public final class Released implements DeliveryState, Outcome {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000026L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:released:list");
+
+    private static final Released INSTANCE = new Released();
+
+    private Released() {
+    }
+
+    @Override
+    public String toString() {
+        return "Released{}";
+    }
+
+    public static Released getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Released;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Section.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Section.java
new file mode 100644
index 0000000..0a0ac94
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Section.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+public interface Section<E> {
+
+    enum SectionType {
+        AmqpSequence,
+        AmqpValue,
+        ApplicationProperties,
+        Data,
+        DeliveryAnnotations,
+        Footer,
+        Header,
+        MessageAnnotations,
+        Properties
+    }
+
+    /**
+     * @return the {@link SectionType} that describes this instance.
+     */
+    SectionType getType();
+
+    /**
+     * @return the Object value contained within the given message {@link Section}.
+     */
+    E getValue();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Source.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Source.java
new file mode 100644
index 0000000..81171e1
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Source.java
@@ -0,0 +1,197 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Source implements Terminus {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000028L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:source:list");
+
+    private String address;
+    private TerminusDurability durable = TerminusDurability.NONE;
+    private TerminusExpiryPolicy expiryPolicy = TerminusExpiryPolicy.SESSION_END;
+    private UnsignedInteger timeout = UnsignedInteger.ZERO;
+    private boolean dynamic;
+    private Map<Symbol, Object> dynamicNodeProperties;
+    private Symbol distributionMode;
+    private Map<Symbol, Object> filter;
+    private Outcome defaultOutcome;
+    private Symbol[] outcomes;
+    private Symbol[] capabilities;
+
+    public Source() {
+    }
+
+    private Source(Source other) {
+        this.address = other.address;
+        this.durable = other.durable;
+        this.expiryPolicy = other.expiryPolicy;
+        this.timeout = other.timeout;
+        this.dynamic = other.dynamic;
+
+        if (other.dynamicNodeProperties != null) {
+            this.dynamicNodeProperties = new HashMap<>(other.dynamicNodeProperties);
+        }
+
+        if (other.capabilities != null) {
+            this.capabilities = other.capabilities.clone();
+        }
+
+        this.distributionMode = other.distributionMode;
+
+        if (other.filter != null) {
+            this.filter = new HashMap<>(other.filter);
+        }
+
+        this.defaultOutcome = other.defaultOutcome;
+
+        if (other.outcomes != null) {
+            this.outcomes = other.outcomes.clone();
+        }
+    }
+
+    @Override
+    public Source copy() {
+        return new Source(this);
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public Source setAddress(String address) {
+        this.address = address;
+        return this;
+    }
+
+    public TerminusDurability getDurable() {
+        return durable;
+    }
+
+    public Source setDurable(TerminusDurability durable) {
+        this.durable = durable == null ? TerminusDurability.NONE : durable;
+        return this;
+    }
+
+    public TerminusExpiryPolicy getExpiryPolicy() {
+        return expiryPolicy;
+    }
+
+    public Source setExpiryPolicy(TerminusExpiryPolicy expiryPolicy) {
+        this.expiryPolicy = expiryPolicy == null ? TerminusExpiryPolicy.SESSION_END : expiryPolicy;
+        return this;
+    }
+
+    public UnsignedInteger getTimeout() {
+        return timeout;
+    }
+
+    public Source setTimeout(UnsignedInteger timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    public boolean isDynamic() {
+        return dynamic;
+    }
+
+    public Source setDynamic(boolean dynamic) {
+        this.dynamic = dynamic;
+        return this;
+    }
+
+    public Map<Symbol, Object> getDynamicNodeProperties() {
+        return dynamicNodeProperties;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Source setDynamicNodeProperties(Map<Symbol, ?> dynamicNodeProperties) {
+        this.dynamicNodeProperties = (Map<Symbol, Object>) dynamicNodeProperties;
+        return this;
+    }
+
+    public Symbol[] getCapabilities() {
+        return capabilities;
+    }
+
+    public Source setCapabilities(Symbol... capabilities) {
+        this.capabilities = capabilities;
+        return this;
+    }
+
+    public Symbol getDistributionMode() {
+        return distributionMode;
+    }
+
+    public Source setDistributionMode(Symbol distributionMode) {
+        this.distributionMode = distributionMode;
+        return this;
+    }
+
+    public Map<Symbol, Object> getFilter() {
+        return filter;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Source setFilter(Map<Symbol, ?> filter) {
+        this.filter = (Map<Symbol, Object>) filter;
+        return this;
+    }
+
+    public Outcome getDefaultOutcome() {
+        return defaultOutcome;
+    }
+
+    public Source setDefaultOutcome(Outcome defaultOutcome) {
+        this.defaultOutcome = defaultOutcome;
+        return this;
+    }
+
+    public Symbol[] getOutcomes() {
+        return outcomes;
+    }
+
+    public Source setOutcomes(Symbol... outcomes) {
+        this.outcomes = outcomes;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Source{" +
+               "address='" + getAddress() + '\'' +
+               ", durable=" + getDurable() +
+               ", expiryPolicy=" + getExpiryPolicy() +
+               ", timeout=" + getTimeout() +
+               ", dynamic=" + isDynamic() +
+               ", dynamicNodeProperties=" + getDynamicNodeProperties() +
+               ", distributionMode=" + distributionMode +
+               ", filter=" + filter +
+               ", defaultOutcome=" + defaultOutcome +
+               ", outcomes=" + (outcomes == null ? null : Arrays.asList(outcomes)) +
+               ", capabilities=" + (getCapabilities() == null ? null : Arrays.asList(getCapabilities())) +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Target.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Target.java
new file mode 100644
index 0000000..979717b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Target.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Target implements Terminus {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000029L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:target:list");
+
+    private String address;
+    private TerminusDurability durable = TerminusDurability.NONE;
+    private TerminusExpiryPolicy expiryPolicy = TerminusExpiryPolicy.SESSION_END;
+    private UnsignedInteger timeout = UnsignedInteger.ZERO;
+    private boolean dynamic;
+    private Map<Symbol, Object> dynamicNodeProperties;
+    private Symbol[] capabilities;
+
+    public Target() {
+    }
+
+    protected Target(Target other) {
+        this.address = other.address;
+        this.durable = other.durable;
+        this.expiryPolicy = other.expiryPolicy;
+        this.timeout = other.timeout;
+        this.dynamic = other.dynamic;
+
+        if (other.dynamicNodeProperties != null) {
+            this.dynamicNodeProperties = new HashMap<>(other.dynamicNodeProperties);
+        }
+
+        if (other.capabilities != null) {
+            this.capabilities = other.capabilities.clone();
+        }
+    }
+
+    @Override
+    public Target copy() {
+        return new Target(this);
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public Target setAddress(String address) {
+        this.address = address;
+        return this;
+    }
+
+    public TerminusDurability getDurable() {
+        return durable;
+    }
+
+    public Target setDurable(TerminusDurability durable) {
+        this.durable = durable == null ? TerminusDurability.NONE : durable;
+        return this;
+    }
+
+    public TerminusExpiryPolicy getExpiryPolicy() {
+        return expiryPolicy;
+    }
+
+    public Target setExpiryPolicy(TerminusExpiryPolicy expiryPolicy) {
+        this.expiryPolicy = expiryPolicy == null ? TerminusExpiryPolicy.SESSION_END : expiryPolicy;
+        return this;
+    }
+
+    public UnsignedInteger getTimeout() {
+        return timeout;
+    }
+
+    public Target setTimeout(UnsignedInteger timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    public boolean isDynamic() {
+        return dynamic;
+    }
+
+    public Target setDynamic(boolean dynamic) {
+        this.dynamic = dynamic;
+        return this;
+    }
+
+    public Map<Symbol, Object> getDynamicNodeProperties() {
+        return dynamicNodeProperties;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Target setDynamicNodeProperties(Map<Symbol, ?> dynamicNodeProperties) {
+        this.dynamicNodeProperties = (Map<Symbol, Object>) dynamicNodeProperties;
+        return this;
+    }
+
+    public Symbol[] getCapabilities() {
+        return capabilities;
+    }
+
+    public Target setCapabilities(Symbol... capabilities) {
+        this.capabilities = capabilities;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Target{" +
+               "address='" + getAddress() + '\'' +
+               ", durable=" + getDurable() +
+               ", expiryPolicy=" + getExpiryPolicy() +
+               ", timeout=" + getTimeout() +
+               ", dynamic=" + isDynamic() +
+               ", dynamicNodeProperties=" + getDynamicNodeProperties() +
+               ", capabilities=" + (getCapabilities() == null ? null : Arrays.asList(getCapabilities())) +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Terminus.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Terminus.java
new file mode 100644
index 0000000..ee89731
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/Terminus.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+public interface Terminus {
+
+    Terminus copy();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/TerminusDurability.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/TerminusDurability.java
new file mode 100644
index 0000000..d0ff62e
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/TerminusDurability.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+
+public enum TerminusDurability {
+
+    NONE, CONFIGURATION, UNSETTLED_STATE;
+
+    public UnsignedInteger getValue() {
+        return UnsignedInteger.valueOf(ordinal());
+    }
+
+    public static TerminusDurability valueOf(UnsignedInteger value) {
+        return TerminusDurability.valueOf(value.intValue());
+    }
+
+    public static TerminusDurability valueOf(long value) {
+        if (value == 0) {
+            return NONE;
+        } else if (value == 1) {
+            return CONFIGURATION;
+        } else if (value == 2) {
+            return UNSETTLED_STATE;
+        }
+
+        throw new IllegalArgumentException("Unknown TerminusDurablity: " + value);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/TerminusExpiryPolicy.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/TerminusExpiryPolicy.java
new file mode 100644
index 0000000..bc46604
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/messaging/TerminusExpiryPolicy.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public enum TerminusExpiryPolicy {
+
+    LINK_DETACH("link-detach"),
+    SESSION_END("session-end"),
+    CONNECTION_CLOSE("connection-close"),
+    NEVER("never");
+
+    private Symbol policy;
+    private static final Map<Symbol, TerminusExpiryPolicy> map = new HashMap<>();
+
+    TerminusExpiryPolicy(String policy) {
+        this.policy = Symbol.valueOf(policy);
+    }
+
+    public Symbol getPolicy() {
+        return policy;
+    }
+
+    static {
+        map.put(LINK_DETACH.getPolicy(), LINK_DETACH);
+        map.put(SESSION_END.getPolicy(), SESSION_END);
+        map.put(CONNECTION_CLOSE.getPolicy(), CONNECTION_CLOSE);
+        map.put(NEVER.getPolicy(), NEVER);
+    }
+
+    public static TerminusExpiryPolicy valueOf(Symbol policy) {
+        TerminusExpiryPolicy expiryPolicy = map.get(policy);
+        if (expiryPolicy == null) {
+            throw new IllegalArgumentException("Unknown TerminusExpiryPolicy: " + policy);
+        }
+        return expiryPolicy;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslChallenge.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslChallenge.java
new file mode 100644
index 0000000..5e2eeeb
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslChallenge.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import java.util.Objects;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class SaslChallenge implements SaslPerformative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000042L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-challenge:list");
+
+    private ProtonBuffer challenge;
+
+    public ProtonBuffer getChallenge() {
+        return challenge;
+    }
+
+    public SaslChallenge setChallenge(Binary challenge) {
+        Objects.requireNonNull(challenge, "The challenge field is mandatory");
+        setChallenge(challenge.asProtonBuffer());
+        return this;
+    }
+
+    public SaslChallenge setChallenge(ProtonBuffer challenge) {
+        Objects.requireNonNull(challenge, "The challenge field is mandatory");
+        this.challenge = challenge;
+        return this;
+    }
+
+    @Override
+    public SaslChallenge copy() {
+        SaslChallenge copy = new SaslChallenge();
+        if (challenge != null) {
+            copy.setChallenge(challenge.copy());
+        }
+        return copy;
+    }
+
+    @Override
+    public String toString() {
+        return "SaslChallenge{" + "challenge=" + challenge + '}';
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.CHALLENGE;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleChallenge(this, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslCode.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslCode.java
new file mode 100644
index 0000000..33161dd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslCode.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import org.apache.qpid.protonj2.types.UnsignedByte;
+
+public enum SaslCode {
+
+    OK, AUTH, SYS, SYS_PERM, SYS_TEMP;
+
+    public UnsignedByte getValue() {
+        return UnsignedByte.valueOf((byte) ordinal());
+    }
+
+    public byte byteValue() {
+        return UnsignedByte.valueOf((byte) ordinal()).byteValue();
+    }
+
+    public static SaslCode valueOf(UnsignedByte v) {
+        return SaslCode.values()[v.byteValue()];
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslInit.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslInit.java
new file mode 100644
index 0000000..ddf7eee
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslInit.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class SaslInit implements SaslPerformative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000041L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-init:list");
+
+    private Symbol mechanism;
+    private ProtonBuffer initialResponse;
+    private String hostname;
+
+    public Symbol getMechanism() {
+        return mechanism;
+    }
+
+    public SaslInit setMechanism(Symbol mechanism) {
+        if (mechanism == null) {
+            throw new NullPointerException("the mechanism field is mandatory");
+        }
+
+        this.mechanism = mechanism;
+        return this;
+    }
+
+    public ProtonBuffer getInitialResponse() {
+        return initialResponse;
+    }
+
+    public SaslInit setInitialResponse(Binary initialResponse) {
+        this.initialResponse = initialResponse == null ? null : initialResponse.asProtonBuffer();
+        return this;
+    }
+
+    public SaslInit setInitialResponse(ProtonBuffer initialResponse) {
+        this.initialResponse = initialResponse;
+        return this;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public SaslInit setHostname(String hostname) {
+        this.hostname = hostname;
+        return this;
+    }
+
+    @Override
+    public SaslInit copy() {
+        SaslInit copy = new SaslInit();
+
+        copy.setHostname(hostname);
+        copy.setInitialResponse(initialResponse == null ? null : initialResponse.copy());
+        if (mechanism != null) {
+            copy.setMechanism(mechanism);
+        }
+
+        return copy;
+    }
+
+    @Override
+    public String toString() {
+        return "SaslInit{" +
+               "mechanism=" + mechanism +
+               ", initialResponse=" + initialResponse +
+               ", hostname='" + hostname + '\'' + '}';
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.INIT;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleInit(this, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslMechanisms.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslMechanisms.java
new file mode 100644
index 0000000..cf9b507
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslMechanisms.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class SaslMechanisms implements SaslPerformative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000040L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-mechanisms:list");
+
+    private Symbol[] saslServerMechanisms;
+
+    public Symbol[] getSaslServerMechanisms() {
+        return saslServerMechanisms;
+    }
+
+    public SaslMechanisms setSaslServerMechanisms(Symbol... saslServerMechanisms) {
+        if (saslServerMechanisms == null) {
+            throw new NullPointerException("the sasl-server-mechanisms field is mandatory");
+        }
+
+        this.saslServerMechanisms = saslServerMechanisms;
+        return this;
+    }
+
+    @Override
+    public SaslMechanisms copy() {
+        SaslMechanisms copy = new SaslMechanisms();
+        if (saslServerMechanisms != null) {
+            copy.setSaslServerMechanisms(Arrays.copyOf(saslServerMechanisms, saslServerMechanisms.length));
+        }
+        return copy;
+    }
+
+    @Override
+    public String toString() {
+        return "SaslMechanisms{" + "saslServerMechanisms=" +
+                    (saslServerMechanisms == null ? null : Arrays.asList(saslServerMechanisms)) + '}';
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.MECHANISMS;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleMechanisms(this, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslOutcome.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslOutcome.java
new file mode 100644
index 0000000..255f2e6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslOutcome.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class SaslOutcome implements SaslPerformative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000044L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-outcome:list");
+
+    private SaslCode code;
+    private ProtonBuffer additionalData;
+
+    public SaslCode getCode() {
+        return code;
+    }
+
+    public SaslOutcome setCode(SaslCode code) {
+        if (code == null) {
+            throw new NullPointerException("the code field is mandatory");
+        }
+
+        this.code = code;
+        return this;
+    }
+
+    public ProtonBuffer getAdditionalData() {
+        return additionalData;
+    }
+
+    public SaslOutcome setAdditionalData(Binary additionalData) {
+        this.additionalData = additionalData == null ? null : additionalData.asProtonBuffer();
+        return this;
+    }
+
+    public SaslOutcome setAdditionalData(ProtonBuffer additionalData) {
+        this.additionalData = additionalData;
+        return this;
+    }
+
+    @Override
+    public SaslOutcome copy() {
+        SaslOutcome copy = new SaslOutcome();
+
+        if (code != null) {
+            copy.setCode(code);
+        }
+        copy.setAdditionalData(additionalData == null ? null : additionalData.copy());
+
+        return copy;
+    }
+
+    @Override
+    public String toString() {
+        return "SaslOutcome{" + "_code=" + code + ", _additionalData=" + additionalData + '}';
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.OUTCOME;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleOutcome(this, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslPerformative.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslPerformative.java
new file mode 100644
index 0000000..6abd63f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslPerformative.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+/**
+ * Marker interface for AMQP Performatives
+ */
+public interface SaslPerformative {
+
+    enum SaslPerformativeType {
+        INIT,
+        MECHANISMS,
+        CHALLENGE,
+        RESPONSE,
+        OUTCOME
+    }
+
+    SaslPerformative copy();
+
+    SaslPerformativeType getPerformativeType();
+
+    interface SaslPerformativeHandler<E> {
+
+        default void handleMechanisms(SaslMechanisms saslMechanisms, E context) {}
+        default void handleInit(SaslInit saslInit, E context) {}
+        default void handleChallenge(SaslChallenge saslChallenge, E context) {}
+        default void handleResponse(SaslResponse saslResponse, E context) {}
+        default void handleOutcome(SaslOutcome saslOutcome, E context) {}
+
+    }
+
+    <E> void invoke(SaslPerformativeHandler<E> handler, E context);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslResponse.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslResponse.java
new file mode 100644
index 0000000..b7ee786
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/security/SaslResponse.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import java.util.Objects;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class SaslResponse implements SaslPerformative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000043L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:sasl-response:list");
+
+    private ProtonBuffer response;
+
+    public ProtonBuffer getResponse() {
+        return response;
+    }
+
+    public SaslResponse setResponse(Binary response) {
+        Objects.requireNonNull(response, "The response field is mandatory");
+        setResponse(response.asProtonBuffer());
+        return this;
+    }
+
+    public SaslResponse setResponse(ProtonBuffer response) {
+        Objects.requireNonNull(response, "The response field is mandatory");
+        this.response = response;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "SaslResponse{" + "response=" + response + '}';
+    }
+
+    @Override
+    public SaslResponse copy() {
+        SaslResponse copy = new SaslResponse();
+        if (response != null) {
+            copy.setResponse(response.copy());
+        }
+        return copy;
+    }
+
+    @Override
+    public SaslPerformativeType getPerformativeType() {
+        return SaslPerformativeType.RESPONSE;
+    }
+
+    @Override
+    public <E> void invoke(SaslPerformativeHandler<E> handler, E context) {
+        handler.handleResponse(this, context);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Coordinator.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Coordinator.java
new file mode 100644
index 0000000..9374c76
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Coordinator.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+
+public final class Coordinator implements Terminus {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000030L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:coordinator:list");
+
+    private Symbol[] capabilities;
+
+    public Coordinator() {
+        super();
+    }
+
+    protected Coordinator(Coordinator other) {
+        if (other.capabilities != null) {
+            this.capabilities = other.capabilities.clone();
+        }
+    }
+
+    public Symbol[] getCapabilities() {
+        return capabilities;
+    }
+
+    public final Coordinator setCapabilities(Symbol... capabilities) {
+        this.capabilities = capabilities;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Coordinator{" + "capabilities=" + (getCapabilities() == null ? null : Arrays.asList(getCapabilities())) + '}';
+    }
+
+    @Override
+    public Coordinator copy() {
+        return new Coordinator(this);
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Declare.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Declare.java
new file mode 100644
index 0000000..f2dee92
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Declare.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Declare {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000031L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:declare:list");
+
+    private GlobalTxId globalId;
+
+    public GlobalTxId getGlobalId() {
+        return globalId;
+    }
+
+    public Declare setGlobalId(GlobalTxId globalId) {
+        this.globalId = globalId;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Declare{" + "globalId=" + globalId + '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Declared.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Declared.java
new file mode 100644
index 0000000..080e229
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Declared.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+public final class Declared implements DeliveryState, Outcome {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000033L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:declared:list");
+
+    private Binary txnId;
+
+    public Binary getTxnId() {
+        return txnId;
+    }
+
+    public Declared setTxnId(Binary txnId) {
+        if (txnId == null) {
+            throw new NullPointerException("the txn-id field is mandatory");
+        }
+
+        this.txnId = txnId;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Declared{" + "txnId=" + txnId + '}';
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Declared;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Discharge.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Discharge.java
new file mode 100644
index 0000000..7f84e88
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/Discharge.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Discharge {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000032L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:discharge:list");
+
+    private Binary txnId;
+    private boolean fail;
+
+    public Binary getTxnId() {
+        return txnId;
+    }
+
+    public Discharge setTxnId(Binary txnId) {
+        if (txnId == null) {
+            throw new NullPointerException("the txn-id field is mandatory");
+        }
+
+        this.txnId = txnId;
+        return this;
+    }
+
+    public boolean getFail() {
+        return fail;
+    }
+
+    public Discharge setFail(boolean fail) {
+        this.fail = fail;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Discharge{" + "txnId=" + txnId + ", fail=" + fail + '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/GlobalTxId.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/GlobalTxId.java
new file mode 100644
index 0000000..d465153
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/GlobalTxId.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+public interface GlobalTxId {
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TransactionErrors.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TransactionErrors.java
new file mode 100644
index 0000000..bca8688
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TransactionErrors.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public interface TransactionErrors {
+
+    final static Symbol UNKNOWN_ID = Symbol.valueOf("amqp:transaction:unknown-id");
+
+    final static Symbol TRANSACTION_ROLLBACK = Symbol.valueOf("amqp:transaction:rollback");
+
+    final static Symbol TRANSACTION_TIMEOUT = Symbol.valueOf("amqp:transaction:timeout");
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TransactionalState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TransactionalState.java
new file mode 100644
index 0000000..ecfc249
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TransactionalState.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+
+public final class TransactionalState implements DeliveryState {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000034L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:transactional-state:list");
+
+    private Binary txnId;
+    private Outcome outcome;
+
+    public Binary getTxnId() {
+        return txnId;
+    }
+
+    public TransactionalState setTxnId(Binary txnId) {
+        if (txnId == null) {
+            throw new NullPointerException("the txn-id field is mandatory");
+        }
+
+        this.txnId = txnId;
+        return this;
+    }
+
+    public Outcome getOutcome() {
+        return outcome;
+    }
+
+    public TransactionalState setOutcome(Outcome outcome) {
+        this.outcome = outcome;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "TransactionalState{" + "txnId=" + txnId + ", outcome=" + outcome + '}';
+    }
+
+    @Override
+    public DeliveryStateType getType() {
+        return DeliveryStateType.Transactional;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TxnCapability.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TxnCapability.java
new file mode 100644
index 0000000..108db23
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transactions/TxnCapability.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public interface TxnCapability {
+
+    final static Symbol LOCAL_TXN = Symbol.valueOf("amqp:local-transactions");
+
+    final static Symbol DISTRIBUTED_TXN = Symbol.valueOf("amqp:distributed-transactions");
+
+    final static Symbol PROMOTABLE_TXN = Symbol.valueOf("amqp:promotable-transactions");
+
+    final static Symbol MULTI_TXNS_PER_SSN = Symbol.valueOf("amqp:multi-txns-per-ssn");
+
+    final static Symbol MULTI_SSNS_PER_TXN = Symbol.valueOf("amqp:multi-ssns-per-txn");
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/AMQPHeader.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/AMQPHeader.java
new file mode 100644
index 0000000..c5f1099
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/AMQPHeader.java
@@ -0,0 +1,331 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import java.nio.ByteBuffer;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBuffer;
+
+/**
+ * Represents the AMQP protocol handshake packet that is sent during the
+ * initial exchange with a remote peer.
+ */
+public final class AMQPHeader {
+
+    static final byte[] PREFIX = new byte[] { 'A', 'M', 'Q', 'P' };
+
+    public static final int PROTOCOL_ID_INDEX = 4;
+    public static final int MAJOR_VERSION_INDEX = 5;
+    public static final int MINOR_VERSION_INDEX = 6;
+    public static final int REVISION_INDEX = 7;
+
+    public static final byte AMQP_PROTOCOL_ID = 0;
+    public static final byte SASL_PROTOCOL_ID = 3;
+
+    public static final int HEADER_SIZE_BYTES = 8;
+
+    private static final AMQPHeader AMQP_HEADER =
+        new AMQPHeader(new byte[] { 'A', 'M', 'Q', 'P', 0, 1, 0, 0 });
+
+    private static final AMQPHeader SASL_HEADER =
+        new AMQPHeader(new byte[] { 'A', 'M', 'Q', 'P', 3, 1, 0, 0 });
+
+    private ProtonBuffer buffer;
+
+    public AMQPHeader() {
+        this(AMQP_HEADER.buffer.duplicate());
+    }
+
+    public AMQPHeader(byte[] headerBytes) {
+        setBuffer(new ProtonByteBuffer(headerBytes), true);
+    }
+
+    public AMQPHeader(ProtonBuffer buffer) {
+        setBuffer(new ProtonByteBuffer(HEADER_SIZE_BYTES, HEADER_SIZE_BYTES).writeBytes(buffer), true);
+    }
+
+    public AMQPHeader(ProtonBuffer buffer, boolean validate) {
+        setBuffer(new ProtonByteBuffer(HEADER_SIZE_BYTES, HEADER_SIZE_BYTES).writeBytes(buffer), validate);
+    }
+
+    public static AMQPHeader getAMQPHeader() {
+        return AMQP_HEADER;
+    }
+
+    public static AMQPHeader getSASLHeader() {
+        return SASL_HEADER;
+    }
+
+    public int getProtocolId() {
+        return buffer.getByte(PROTOCOL_ID_INDEX) & 0xFF;
+    }
+
+    public int getMajor() {
+        return buffer.getByte(MAJOR_VERSION_INDEX) & 0xFF;
+    }
+
+    public int getMinor() {
+        return buffer.getByte(MINOR_VERSION_INDEX) & 0xFF;
+    }
+
+    public int getRevision() {
+        return buffer.getByte(REVISION_INDEX) & 0xFF;
+    }
+
+    public ProtonBuffer getBuffer() {
+        return buffer.copy();
+    }
+
+    public byte[] toArray() {
+        if (buffer != null) {
+            final byte[] copy = new byte[buffer.getReadableBytes()];
+            buffer.getBytes(0, copy);
+            return copy;
+        } else {
+            return null;
+        }
+    }
+
+    public ByteBuffer toByteBuffer() {
+        if (buffer != null) {
+            final byte[] copy = new byte[buffer.getReadableBytes()];
+            buffer.getBytes(0, copy);
+            return ByteBuffer.wrap(copy);
+        } else {
+            return null;
+        }
+    }
+
+    public byte getByteAt(int i) {
+        return buffer.getByte(i);
+    }
+
+    public boolean hasValidPrefix() {
+        return startsWith(buffer, PREFIX);
+    }
+
+    public boolean isSaslHeader() {
+        return getProtocolId() == SASL_PROTOCOL_ID;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((buffer == null) ? 0 : buffer.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        AMQPHeader other = (AMQPHeader) obj;
+        if (buffer == null) {
+            if (other.buffer != null) {
+                return false;
+            }
+        } else if (!buffer.equals(other.buffer)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < buffer.getReadableBytes(); ++i) {
+            char value = (char) buffer.getByte(i);
+            if (Character.isLetter(value)) {
+                builder.append(value);
+            } else {
+                builder.append(",");
+                builder.append((int) value);
+            }
+        }
+        return builder.toString();
+    }
+
+    private boolean startsWith(ProtonBuffer buffer, byte[] value) {
+        if (buffer == null || buffer.getReadableBytes() < value.length) {
+            return false;
+        }
+
+        for (int i = 0; i < value.length; ++i) {
+            if (buffer.getByte(i) != value[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private AMQPHeader setBuffer(ProtonBuffer value, boolean validate) {
+        if (validate) {
+            if (value.getReadableBytes() != 8 || !startsWith(value, PREFIX)) {
+                throw new IllegalArgumentException("Not an AMQP header buffer");
+            }
+
+            validateProtocolByte(value.getByte(PROTOCOL_ID_INDEX));
+            validateMajorVersionByte(value.getByte(MAJOR_VERSION_INDEX));
+            validateMinorVersionByte(value.getByte(MINOR_VERSION_INDEX));
+            validateRevisionByte(value.getByte(REVISION_INDEX));
+        }
+
+        buffer = value;
+        return this;
+    }
+
+    /**
+     * Called to validate a byte according to a given index within the AMQP Header
+     *
+     * If the index is outside the range of the header size an {@link IndexOutOfBoundsException}
+     * will be thrown.
+     *
+     * @param index
+     *      The index in the header where the byte should be validated.
+     * @param value
+     *      The value to check validity of in the given index in the AMQP Header.
+     *
+     * @throws IllegalArgumentException if the value is not valid for the index given in the AMQP header
+     * @throws IndexOutOfBoundsException if the index value is greater than the AMQP header size.
+     */
+    public static void validateByte(int index, byte value) {
+        switch (index) {
+            case 0:
+                validatePrefixByte1(value);
+                break;
+            case 1:
+                validatePrefixByte2(value);
+                break;
+            case 2:
+                validatePrefixByte3(value);
+                break;
+            case 3:
+                validatePrefixByte4(value);
+                break;
+            case 4:
+                validateProtocolByte(value);
+                break;
+            case 5:
+                validateMajorVersionByte(value);
+                break;
+            case 6:
+                validateMinorVersionByte(value);
+                break;
+            case 7:
+                validateRevisionByte(value);
+                break;
+            default:
+                throw new IndexOutOfBoundsException("Invalid AMQP Header byte index provided to validation method: " + index);
+        }
+    }
+
+    private static void validatePrefixByte1(byte value) {
+        if (value != PREFIX[0]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(1) specified %d : expected %d", value, PREFIX[0]));
+        }
+    }
+
+    private static void validatePrefixByte2(byte value) {
+        if (value != PREFIX[1]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(2) specified %d : expected %d", value, PREFIX[1]));
+        }
+    }
+
+    private static void validatePrefixByte3(byte value) {
+        if (value != PREFIX[2]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(3) specified %d : expected %d", value, PREFIX[2]));
+        }
+    }
+
+    private static void validatePrefixByte4(byte value) {
+        if (value != PREFIX[3]) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid header byte(4) specified %d : expected %d", value, PREFIX[3]));
+        }
+    }
+
+    private static void validateProtocolByte(byte value) {
+        if (value != AMQP_PROTOCOL_ID && value != SASL_PROTOCOL_ID) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid protocol Id specified %d : expected one of %d or %d",
+                value, AMQP_PROTOCOL_ID, SASL_PROTOCOL_ID));
+        }
+    }
+
+    private static void validateMajorVersionByte(byte value) {
+        if (value != 1) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid Major version specified %d : expected %d", value, 1));
+        }
+    }
+
+    private static void validateMinorVersionByte(byte value) {
+        if (value != 0) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid Minor version specified %d : expected %d", value, 0));
+        }
+    }
+
+    private static void validateRevisionByte(byte value) {
+        if (value != 0) {
+            throw new IllegalArgumentException(String.format(
+                "Invalid revision specified %d : expected %d", value, 0));
+        }
+    }
+
+    /**
+     * Provide this AMQP Header with a handler that will process the given AMQP header
+     * depending on the protocol type the correct handler method is invoked.
+     *
+     * @param handler
+     *      The {@link HeaderHandler} instance to use to process the header.
+     * @param context
+     *      A context object to pass along with the header.
+     *
+     * @param <E> The type that will be passed as the context for this event
+     */
+    public <E> void invoke(HeaderHandler<E> handler, E context) {
+        if (isSaslHeader()) {
+            handler.handleSASLHeader(this, context);
+        } else {
+            handler.handleAMQPHeader(this, context);
+        }
+    }
+
+    public interface HeaderHandler<E> {
+
+        default void handleAMQPHeader(AMQPHeader header, E context) {}
+
+        default void handleSASLHeader(AMQPHeader header, E context) {}
+
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/AmqpError.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/AmqpError.java
new file mode 100644
index 0000000..0f8310f
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/AmqpError.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public interface AmqpError {
+
+    /**
+     * An internal error occurred. Operator intervention might be necessary to resume normal operation.
+     */
+    Symbol INTERNAL_ERROR = Symbol.valueOf("amqp:internal-error");
+
+    /**
+     * A peer attempted to work with a remote entity that does not exist.
+     */
+    Symbol NOT_FOUND = Symbol.valueOf("amqp:not-found");
+
+    /**
+     * A peer attempted to work with a remote entity to which it has no access due to security settings.
+     */
+    Symbol UNAUTHORIZED_ACCESS = Symbol.valueOf("amqp:unauthorized-access");
+
+    /**
+     * Data could not be decoded.
+     */
+    Symbol DECODE_ERROR = Symbol.valueOf("amqp:decode-error");
+
+    /**
+     * A peer exceeded its resource allocation.
+     */
+    Symbol RESOURCE_LIMIT_EXCEEDED = Symbol.valueOf("amqp:resource-limit-exceeded");
+
+    /**
+     * The peer tried to use a frame in a manner that is inconsistent with the semantics defined in the specification.
+     */
+    Symbol NOT_ALLOWED = Symbol.valueOf("amqp:not-allowed");
+
+    /**
+     * An invalid field was passed in a frame body, and the operation could not proceed.
+     */
+    Symbol INVALID_FIELD = Symbol.valueOf("amqp:invalid-field");
+
+    /**
+     * The peer tried to use functionality that is not implemented in its partner.
+     */
+    Symbol NOT_IMPLEMENTED = Symbol.valueOf("amqp:not-implemented");
+
+    /**
+     * The client attempted to work with a server entity to which it has no access because another client is working with it.
+     */
+    Symbol RESOURCE_LOCKED = Symbol.valueOf("amqp:resource-locked");
+
+    /**
+     * The client made a request that was not allowed because some precondition failed.
+     */
+    Symbol PRECONDITION_FAILED = Symbol.valueOf("amqp:precondition-failed");
+
+    /**
+     * A server entity the client is working with has been deleted.
+     */
+    Symbol RESOURCE_DELETED = Symbol.valueOf("amqp:resource-deleted");
+
+    /**
+     * The peer sent a frame that is not permitted in the current state.
+     */
+    Symbol ILLEGAL_STATE = Symbol.valueOf("amqp:illegal-state");
+
+    /**
+     * The peer cannot send a frame because the smallest encoding of the performative with the currently valid
+     * values would be too large to fit within a frame of the agreed maximum frame size. When transferring a message
+     * the message data can be sent in multiple transfer frames thereby avoiding this error. Similarly when attaching
+     * a link with a large unsettled map the endpoint MAY make use of the incomplete-unsettled flag to avoid the need
+     * for overly large frames.
+     */
+    Symbol FRAME_SIZE_TOO_SMALL = Symbol.valueOf("amqp:frame-size-too-small");
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Attach.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Attach.java
new file mode 100644
index 0000000..850e9d6
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Attach.java
@@ -0,0 +1,468 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+
+public final class Attach implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000012L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:attach:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int NAME = 1;
+    private static int HANDLE = 2;
+    private static int ROLE = 4;
+    private static int SENDER_SETTLE_MODE = 8;
+    private static int RECEIVER_SETTLE_MODE = 16;
+    private static int SOURCE = 32;
+    private static int TARGET = 64;
+    private static int UNSETTLED = 128;
+    private static int INCOMPLETE_UNSETTLED = 256;
+    private static int INITIAL_DELIVERY_COUNT = 512;
+    private static int MAX_MESSAGE_SIZE = 1024;
+    private static int OFFERED_CAPABILITIES = 2048;
+    private static int DESIRED_CAPABILITIES = 4096;
+    private static int PROPERTIES = 8192;
+
+    private int modified = 0;
+
+    // TODO - Consider using the matching signed types instead of next largest
+    //        for these values as in most cases we don't actually care about sign.
+    //        In the cases we do care we could just do the math and make these
+    //        interfaces simpler and not check all over the place for overflow.
+
+    private String name;
+    private long handle;
+    private Role role = Role.SENDER;
+    private SenderSettleMode sndSettleMode = SenderSettleMode.MIXED;
+    private ReceiverSettleMode rcvSettleMode = ReceiverSettleMode.FIRST;
+    private Source source;
+    private Terminus target;
+    private Map<Binary, DeliveryState> unsettled;
+    private boolean incompleteUnsettled;
+    private long initialDeliveryCount;
+    private UnsignedLong maxMessageSize;
+    private Symbol[] offeredCapabilities;
+    private Symbol[] desiredCapabilities;
+    private Map<Symbol, Object> properties;
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.ATTACH;
+    }
+
+    @Override
+    public Attach copy() {
+        Attach copy = new Attach();
+
+        copy.name = name;
+        copy.handle = handle;
+        copy.role = role;
+        copy.sndSettleMode = sndSettleMode;
+        copy.rcvSettleMode = rcvSettleMode;
+        copy.source = source;
+        copy.target = target;
+        if (unsettled != null) {
+            copy.unsettled = new LinkedHashMap<>(unsettled);
+        }
+        copy.incompleteUnsettled = incompleteUnsettled;
+        copy.initialDeliveryCount = initialDeliveryCount;
+        copy.maxMessageSize = maxMessageSize;
+        if (offeredCapabilities != null) {
+            copy.offeredCapabilities = Arrays.copyOf(offeredCapabilities, offeredCapabilities.length);
+        }
+        if (desiredCapabilities != null) {
+            copy.desiredCapabilities = Arrays.copyOf(desiredCapabilities, desiredCapabilities.length);
+        }
+        if (properties != null) {
+            copy.properties = new LinkedHashMap<>(properties);
+        }
+
+        copy.modified = modified;
+
+        return copy;
+    }
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasName() {
+        return (modified & NAME) == NAME;
+    }
+
+    public boolean hasHandle() {
+        return (modified & HANDLE) == HANDLE;
+    }
+
+    public boolean hasRole() {
+        return (modified & ROLE) == ROLE;
+    }
+
+    public boolean hasSenderSettleMode() {
+        return (modified & SENDER_SETTLE_MODE) == SENDER_SETTLE_MODE;
+    }
+
+    public boolean hasReceiverSettleMode() {
+        return (modified & RECEIVER_SETTLE_MODE) == RECEIVER_SETTLE_MODE;
+    }
+
+    public boolean hasSource() {
+        return (modified & SOURCE) == SOURCE;
+    }
+
+    public boolean hasTargetOrCoordinator() {
+        return (modified & TARGET) == TARGET;
+    }
+
+    public boolean hasTarget() {
+        return (modified & TARGET) == TARGET && target instanceof Target;
+    }
+
+    public boolean hasCoordinator() {
+        return (modified & TARGET) == TARGET && target instanceof Coordinator;
+    }
+
+    public boolean hasUnsettled() {
+        return (modified & UNSETTLED) == UNSETTLED;
+    }
+
+    public boolean hasIncompleteUnsettled() {
+        return (modified & INCOMPLETE_UNSETTLED) == INCOMPLETE_UNSETTLED;
+    }
+
+    public boolean hasInitialDeliveryCount() {
+        return (modified & INITIAL_DELIVERY_COUNT) == INITIAL_DELIVERY_COUNT;
+    }
+
+    public boolean hasMaxMessageSize() {
+        return (modified & MAX_MESSAGE_SIZE) == MAX_MESSAGE_SIZE;
+    }
+
+    public boolean hasOfferedCapabilites() {
+        return (modified & OFFERED_CAPABILITIES) == OFFERED_CAPABILITIES;
+    }
+
+    public boolean hasDesiredCapabilites() {
+        return (modified & DESIRED_CAPABILITIES) == DESIRED_CAPABILITIES;
+    }
+
+    public boolean hasProperties() {
+        return (modified & PROPERTIES) == PROPERTIES;
+    }
+
+    //----- Access to the member data with state checks
+
+    public String getName() {
+        return name;
+    }
+
+    public Attach setName(String name) {
+        if (name == null) {
+            throw new NullPointerException("the name field is mandatory");
+        }
+
+        modified |= NAME;
+
+        this.name = name;
+        return this;
+    }
+
+    public long getHandle() {
+        return handle;
+    }
+
+    public Attach setHandle(int handle) {
+        modified |= HANDLE;
+        this.handle = Integer.toUnsignedLong(handle);
+        return this;
+    }
+
+    public Attach setHandle(long handle) {
+        if (handle < 0 || handle > UINT_MAX) {
+            throw new IllegalArgumentException("The Handle value given is out of range: " + handle);
+        } else {
+            modified |= HANDLE;
+        }
+
+        this.handle = handle;
+        return this;
+    }
+
+    public Role getRole() {
+        return role;
+    }
+
+    public Attach setRole(Role role) {
+        if (role == null) {
+            throw new NullPointerException("Role cannot be null");
+        }
+
+        modified |= ROLE;
+
+        this.role = role;
+        return this;
+    }
+
+    public SenderSettleMode getSenderSettleMode() {
+        return sndSettleMode;
+    }
+
+    public Attach setSenderSettleMode(SenderSettleMode sndSettleMode) {
+        if (sndSettleMode != null) {
+            modified |= SENDER_SETTLE_MODE;
+        } else {
+            modified &= ~SENDER_SETTLE_MODE;
+        }
+
+        this.sndSettleMode = sndSettleMode == null ? SenderSettleMode.MIXED : sndSettleMode;
+        return this;
+    }
+
+    public ReceiverSettleMode getReceiverSettleMode() {
+        return rcvSettleMode;
+    }
+
+    public Attach setReceiverSettleMode(ReceiverSettleMode rcvSettleMode) {
+        if (rcvSettleMode != null) {
+            modified |= RECEIVER_SETTLE_MODE;
+        } else {
+            modified &= ~RECEIVER_SETTLE_MODE;
+        }
+
+        this.rcvSettleMode = rcvSettleMode == null ? ReceiverSettleMode.FIRST : rcvSettleMode;
+        return this;
+    }
+
+    public Source getSource() {
+        return source;
+    }
+
+    public Attach setSource(Source source) {
+        if (source != null) {
+            modified |= SOURCE;
+        } else {
+            modified &= ~SOURCE;
+        }
+
+        this.source = source;
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends Terminus> T getTarget() {
+        return (T) target;
+    }
+
+    public Attach setTarget(Terminus target) {
+        if (target instanceof Target) {
+            setTarget((Target) target);
+        } else if (target instanceof Coordinator) {
+            setTarget((Coordinator) target);
+        } else {
+            throw new IllegalArgumentException("Cannot set Target terminus to given value: " + target);
+        }
+
+        return this;
+    }
+
+    public Attach setTarget(Target target) {
+        if (target != null) {
+            modified |= TARGET;
+        } else {
+            modified &= ~TARGET;
+        }
+
+        this.target = target;
+        return this;
+    }
+
+    public Attach setTarget(Coordinator target) {
+        if (target != null) {
+            modified |= TARGET;
+        } else {
+            modified &= ~TARGET;
+        }
+
+        this.target = target;
+        return this;
+    }
+
+    public Attach setCoordinator(Coordinator target) {
+        if (target != null) {
+            modified |= TARGET;
+        } else {
+            modified &= ~TARGET;
+        }
+
+        this.target = target;
+        return this;
+    }
+
+    public Map<Binary, DeliveryState> getUnsettled() {
+        return unsettled;
+    }
+
+    public Attach setUnsettled(Map<Binary, DeliveryState> unsettled) {
+        if (unsettled != null) {
+            modified |= UNSETTLED;
+        } else {
+            modified &= ~UNSETTLED;
+        }
+
+        this.unsettled = unsettled;
+        return this;
+    }
+
+    public boolean getIncompleteUnsettled() {
+        return incompleteUnsettled;
+    }
+
+    public Attach setIncompleteUnsettled(boolean incompleteUnsettled) {
+        this.modified |= INCOMPLETE_UNSETTLED;
+        this.incompleteUnsettled = incompleteUnsettled;
+        return this;
+    }
+
+    public long getInitialDeliveryCount() {
+        return initialDeliveryCount;
+    }
+
+    public Attach setInitialDeliveryCount(int initialDeliveryCount) {
+        modified |= INITIAL_DELIVERY_COUNT;
+        this.initialDeliveryCount = Integer.toUnsignedLong(initialDeliveryCount);
+        return this;
+    }
+
+    public Attach setInitialDeliveryCount(long initialDeliveryCount) {
+        if (initialDeliveryCount < 0 || initialDeliveryCount > UINT_MAX) {
+            throw new IllegalArgumentException("The initial delivery count value given is out of range: " + handle);
+        } else {
+            modified |= INITIAL_DELIVERY_COUNT;
+        }
+
+        this.initialDeliveryCount = initialDeliveryCount;
+        return this;
+    }
+
+    public UnsignedLong getMaxMessageSize() {
+        return maxMessageSize;
+    }
+
+    public Attach setMaxMessageSize(long maxMessageSize) {
+        return setMaxMessageSize(UnsignedLong.valueOf(maxMessageSize));
+    }
+
+    public Attach setMaxMessageSize(UnsignedLong maxMessageSize) {
+        if (maxMessageSize != null) {
+            modified |= MAX_MESSAGE_SIZE;
+        } else {
+            modified &= ~MAX_MESSAGE_SIZE;
+        }
+
+        this.maxMessageSize = maxMessageSize;
+        return this;
+    }
+
+    public Symbol[] getOfferedCapabilities() {
+        return offeredCapabilities;
+    }
+
+    public Attach setOfferedCapabilities(Symbol... offeredCapabilities) {
+        if (offeredCapabilities != null) {
+            modified |= OFFERED_CAPABILITIES;
+        } else {
+            modified &= ~OFFERED_CAPABILITIES;
+        }
+
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    public Symbol[] getDesiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    public Attach setDesiredCapabilities(Symbol... desiredCapabilities) {
+        if (desiredCapabilities != null) {
+            modified |= DESIRED_CAPABILITIES;
+        } else {
+            modified &= ~DESIRED_CAPABILITIES;
+        }
+
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    public Map<Symbol, Object> getProperties() {
+        return properties;
+    }
+
+    public Attach setProperties(Map<Symbol, Object> properties) {
+        if (properties != null) {
+            modified |= PROPERTIES;
+        } else {
+            modified &= ~PROPERTIES;
+        }
+
+        this.properties = properties;
+        return this;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleAttach(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Attach{" +
+            "name='" + name + '\'' +
+            ", handle=" + handle +
+            ", role=" + role +
+            ", sndSettleMode=" + sndSettleMode +
+            ", rcvSettleMode=" + rcvSettleMode +
+            ", source=" + source +
+            ", target=" + target +
+            ", unsettled=" + unsettled +
+            ", incompleteUnsettled=" + incompleteUnsettled +
+            ", initialDeliveryCount=" + initialDeliveryCount +
+            ", maxMessageSize=" + maxMessageSize +
+            ", offeredCapabilities=" + (offeredCapabilities == null ? null : Arrays.asList(offeredCapabilities)) +
+            ", desiredCapabilities=" + (desiredCapabilities == null ? null : Arrays.asList(desiredCapabilities)) +
+            ", properties=" + properties + '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Begin.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Begin.java
new file mode 100644
index 0000000..ee5e57b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Begin.java
@@ -0,0 +1,295 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+public final class Begin implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000011L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:begin:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int REMOTE_CHANNEL = 1;
+    private static int NEXT_OUTGOING_ID = 2;
+    private static int INCOMING_WINDOW = 4;
+    private static int OUTGOING_WINDOW = 8;
+    private static int HANDLE_MAX = 16;
+    private static int OFFERED_CAPABILITIES = 32;
+    private static int DESIRED_CAPABILITIES = 64;
+    private static int PROPERTIES = 128;
+
+    private int modified = 0;
+
+    // TODO - Consider using the matching signed types instead of next largest
+    //        for these values as in most cases we don't actually care about sign.
+    //        In the cases we do care we could just do the math and make these
+    //        interfaces simpler and not check all over the place for overflow.
+
+    private int remoteChannel;
+    private long nextOutgoingId;
+    private long incomingWindow;
+    private long outgoingWindow;
+    private long handleMax = UnsignedInteger.MAX_VALUE.longValue();
+    private Symbol[] offeredCapabilities;
+    private Symbol[] desiredCapabilities;
+    private Map<Symbol, Object> properties;
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.BEGIN;
+    }
+
+    @Override
+    public Begin copy() {
+        Begin copy = new Begin();
+
+        copy.remoteChannel = remoteChannel;
+        copy.nextOutgoingId = nextOutgoingId;
+        copy.incomingWindow = incomingWindow;
+        copy.outgoingWindow = outgoingWindow;
+        copy.handleMax = handleMax;
+        if (offeredCapabilities != null) {
+            copy.offeredCapabilities = Arrays.copyOf(offeredCapabilities, offeredCapabilities.length);
+        }
+        if (desiredCapabilities != null) {
+            copy.desiredCapabilities = Arrays.copyOf(desiredCapabilities, desiredCapabilities.length);
+        }
+        if (properties != null) {
+            copy.properties = new LinkedHashMap<>(properties);
+        }
+        copy.modified = modified;
+
+        return copy;
+    }
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasRemoteChannel() {
+        return (modified & REMOTE_CHANNEL) == REMOTE_CHANNEL;
+    }
+
+    public boolean hasNextOutgoingId() {
+        return (modified & NEXT_OUTGOING_ID) == NEXT_OUTGOING_ID;
+    }
+
+    public boolean hasIncomingWindow() {
+        return (modified & INCOMING_WINDOW) == INCOMING_WINDOW;
+    }
+
+    public boolean hasOutgoingWindow() {
+        return (modified & OUTGOING_WINDOW) == OUTGOING_WINDOW;
+    }
+
+    public boolean hasHandleMax() {
+        return (modified & HANDLE_MAX) == HANDLE_MAX;
+    }
+
+    public boolean hasOfferedCapabilites() {
+        return (modified & OFFERED_CAPABILITIES) == OFFERED_CAPABILITIES;
+    }
+
+    public boolean hasDesiredCapabilites() {
+        return (modified & DESIRED_CAPABILITIES) == DESIRED_CAPABILITIES;
+    }
+
+    public boolean hasProperties() {
+        return (modified & PROPERTIES) == PROPERTIES;
+    }
+
+    //----- Access to the member data with state checks
+
+    public int getRemoteChannel() {
+        return remoteChannel;
+    }
+
+    public Begin setRemoteChannel(int remoteChannel) {
+        if (remoteChannel < 0 || remoteChannel > UnsignedShort.MAX_VALUE.intValue()) {
+            throw new IllegalArgumentException("Remote channel value given is out of range: " + remoteChannel);
+        } else {
+            modified |= REMOTE_CHANNEL;
+        }
+
+        this.remoteChannel = remoteChannel;
+        return this;
+    }
+
+    public long getNextOutgoingId() {
+        return nextOutgoingId;
+    }
+
+    public Begin setNextOutgoingId(int nextOutgoingId) {
+        modified |= NEXT_OUTGOING_ID;
+        this.nextOutgoingId = Integer.toUnsignedLong(nextOutgoingId);
+        return this;
+    }
+
+    public Begin setNextOutgoingId(long nextOutgoingId) {
+        if (nextOutgoingId < 0 || nextOutgoingId > UINT_MAX) {
+            throw new IllegalArgumentException("Next Outgoing Id value given is out of range: " + nextOutgoingId);
+        } else {
+            modified |= NEXT_OUTGOING_ID;
+        }
+
+        this.nextOutgoingId = nextOutgoingId;
+        return this;
+    }
+
+    public long getIncomingWindow() {
+        return incomingWindow;
+    }
+
+    public Begin setIncomingWindow(int incomingWindow) {
+        modified |= INCOMING_WINDOW;
+        this.incomingWindow = Integer.toUnsignedLong(incomingWindow);
+        return this;
+    }
+
+    public Begin setIncomingWindow(long incomingWindow) {
+        if (incomingWindow < 0 || incomingWindow > UINT_MAX) {
+            throw new IllegalArgumentException("Incoming Window value given is out of range: " + incomingWindow);
+        } else {
+            modified |= INCOMING_WINDOW;
+        }
+
+        this.incomingWindow = incomingWindow;
+        return this;
+    }
+
+    public long getOutgoingWindow() {
+        return outgoingWindow;
+    }
+
+    public Begin setOutgoingWindow(int outgoingWindow) {
+        modified |= OUTGOING_WINDOW;
+        this.outgoingWindow = Integer.toUnsignedLong(outgoingWindow);
+        return this;
+    }
+
+    public Begin setOutgoingWindow(long outgoingWindow) {
+        if (outgoingWindow < 0 || outgoingWindow > UINT_MAX) {
+            throw new IllegalArgumentException("Incoming Window value given is out of range: " + outgoingWindow);
+        } else {
+            modified |= OUTGOING_WINDOW;
+        }
+
+        this.outgoingWindow = outgoingWindow;
+        return this;
+    }
+
+    public long getHandleMax() {
+        return handleMax;
+    }
+
+    public Begin setHandleMax(int handleMax) {
+        modified |= HANDLE_MAX;
+        this.handleMax = Integer.toUnsignedLong(handleMax);
+        return this;
+    }
+
+    public Begin setHandleMax(long handleMax) {
+        if (handleMax < 0 || handleMax > UINT_MAX) {
+            throw new IllegalArgumentException("Handle Max value given is out of range: " + handleMax);
+        } else {
+            modified |= HANDLE_MAX;
+        }
+
+        this.handleMax = handleMax;
+        return this;
+    }
+
+    public Symbol[] getOfferedCapabilities() {
+        return offeredCapabilities;
+    }
+
+    public Begin setOfferedCapabilities(Symbol... offeredCapabilities) {
+        if (offeredCapabilities != null) {
+            modified |= OFFERED_CAPABILITIES;
+        } else {
+            modified &= ~OFFERED_CAPABILITIES;
+        }
+
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    public Symbol[] getDesiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    public Begin setDesiredCapabilities(Symbol... desiredCapabilities) {
+        if (desiredCapabilities != null) {
+            modified |= DESIRED_CAPABILITIES;
+        } else {
+            modified &= ~DESIRED_CAPABILITIES;
+        }
+
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    public Map<Symbol, Object> getProperties() {
+        return properties;
+    }
+
+    public Begin setProperties(Map<Symbol, Object> properties) {
+        if (properties != null) {
+            modified |= PROPERTIES;
+        } else {
+            modified &= ~PROPERTIES;
+        }
+
+        this.properties = properties;
+        return this;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleBegin(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Begin{" +
+               "remoteChannel=" + remoteChannel +
+               ", nextOutgoingId=" + nextOutgoingId +
+               ", incomingWindow=" + incomingWindow +
+               ", outgoingWindow=" + outgoingWindow +
+               ", handleMax=" + handleMax +
+               ", offeredCapabilities=" + (offeredCapabilities == null ? null : Arrays.asList(offeredCapabilities)) +
+               ", desiredCapabilities=" + (desiredCapabilities == null ? null : Arrays.asList(desiredCapabilities)) +
+               ", properties=" + properties +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Close.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Close.java
new file mode 100644
index 0000000..47652bd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Close.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Close implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000018L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:close:list");
+
+    private ErrorCondition error;
+
+    public ErrorCondition getError() {
+        return error;
+    }
+
+    public Close setError(ErrorCondition error) {
+        this.error = error;
+        return this;
+    }
+
+    @Override
+    public Close copy() {
+        Close copy = new Close();
+        copy.setError(error == null ? null : error.copy());
+        return copy;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.CLOSE;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleClose(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Close{" + "error=" + error + '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ConnectionError.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ConnectionError.java
new file mode 100644
index 0000000..b331e1b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ConnectionError.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public interface ConnectionError {
+
+    /**
+     * An operator intervened to close the connection for some reason. The client could retry at some later date.
+     */
+    Symbol CONNECTION_FORCED = Symbol.valueOf("amqp:connection:forced");
+
+    /**
+     * A valid frame header cannot be formed from the incoming byte stream.
+     */
+    Symbol FRAMING_ERROR = Symbol.valueOf("amqp:connection:framing-error");
+
+    /**
+     * The container is no longer available on the current connection. The peer SHOULD
+     * attempt reconnection to the container using the details provided in the info map.
+     * <br>
+     * <ul>
+     *   <li>hostname</li>
+     *     <ul>
+     *       <li>the hostname of the container hosting the terminus. This is the value that SHOULD be
+     *           supplied in the hostname field of the open frame, and during SASL and TLS negotiation
+     *           (if used).
+     *       </li>
+     *     </ul>
+     *   <li>network-host</li>
+     *     <ul>
+     *       <li>the DNS hostname or IP address of the machine hosting the container.</li>
+     *     </ul>
+     *   <li>port</li>
+     *     <ul>
+     *       <li>the port number on the machine hosting the container.</li>
+     *     </ul>
+     * </ul>
+     */
+    Symbol REDIRECT = Symbol.valueOf("amqp:connection:redirect");
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/DeliveryState.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/DeliveryState.java
new file mode 100644
index 0000000..c24860b
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/DeliveryState.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+/**
+ * Describes the state of a delivery at a link end-point.
+ *
+ * Note that the the sender is the owner of the state.
+ * The receiver merely influences the state.
+ */
+public interface DeliveryState {
+
+    enum DeliveryStateType {
+        Accepted,
+        Declared,
+        Modified,
+        Received,
+        Rejected,
+        Released,
+        Transactional
+    }
+
+    /**
+     * @return the {@link DeliveryStateType} that this instance represents.
+     */
+    DeliveryStateType getType();
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Detach.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Detach.java
new file mode 100644
index 0000000..0003f85
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Detach.java
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Detach implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000016L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:detach:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int HANDLE = 1;
+    private static int CLOSED = 2;
+    private static int ERROR = 4;
+
+    private int modified = 0;
+
+    // TODO - Consider using the matching signed types instead of next largest
+    //        for these values as in most cases we don't actually care about sign.
+    //        In the cases we do care we could just do the math and make these
+    //        interfaces simpler and not check all over the place for overflow.
+
+    private long handle;
+    private boolean closed;
+    private ErrorCondition error;
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasHandle() {
+        return (modified & HANDLE) == HANDLE;
+    }
+
+    public boolean hasClosed() {
+        return (modified & CLOSED) == CLOSED;
+    }
+
+    public boolean hasError() {
+        return (modified & ERROR) == ERROR;
+    }
+
+    //----- Access to the member data with state checks
+
+    public long getHandle() {
+        return handle;
+    }
+
+    public Detach setHandle(long handle) {
+        if (handle < 0 || handle > UINT_MAX) {
+            throw new IllegalArgumentException("The Handle value given is out of range: " + handle);
+        } else {
+            modified |= HANDLE;
+        }
+
+        this.handle = handle;
+        return this;
+    }
+
+    public boolean getClosed() {
+        return closed;
+    }
+
+    public Detach setClosed(boolean closed) {
+        this.modified |= CLOSED;
+        this.closed = closed;
+        return this;
+    }
+
+    public ErrorCondition getError() {
+        return error;
+    }
+
+    public Detach setError(ErrorCondition error) {
+        if (error != null) {
+            modified |= ERROR;
+        } else {
+            modified &= ~ERROR;
+        }
+        this.error = error;
+        return this;
+    }
+
+    @Override
+    public Detach copy() {
+        Detach copy = new Detach();
+
+        copy.handle = handle;
+        copy.closed = closed;
+        copy.error = error == null ? null : error.copy();
+        copy.modified = modified;
+
+        return copy;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.DETACH;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleDetach(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Detach{" +
+               "handle=" + handle +
+               ", closed=" + closed +
+               ", error=" + error +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Disposition.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Disposition.java
new file mode 100644
index 0000000..83b92c5
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Disposition.java
@@ -0,0 +1,258 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Disposition implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000015L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:disposition:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int ROLE = 1;
+    private static int FIRST = 2;
+    private static int LAST = 4;
+    private static int SETTLED = 8;
+    private static int STATE = 16;
+    private static int BATCHABLE = 32;
+
+    private int modified = 0;
+
+    private Role role = Role.SENDER;
+    private long first;
+    private long last;
+    private boolean settled;
+    private DeliveryState state;
+    private boolean batchable;
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasRole() {
+        return (modified & ROLE) == ROLE;
+    }
+
+    public boolean hasFirst() {
+        return (modified & FIRST) == FIRST;
+    }
+
+    public boolean hasLast() {
+        return (modified & LAST) == LAST;
+    }
+
+    public boolean hasSettled() {
+        return (modified & SETTLED) == SETTLED;
+    }
+
+    public boolean hasState() {
+        return (modified & STATE) == STATE;
+    }
+
+    public boolean hasBatchable() {
+        return (modified & BATCHABLE) == BATCHABLE;
+    }
+
+    //----- Access to the member data with state checks
+
+    public Role getRole() {
+        return role;
+    }
+
+    public Disposition setRole(Role role) {
+        if (role == null) {
+            throw new NullPointerException("Role cannot be null");
+        } else {
+            modified |= ROLE;
+        }
+
+        this.role = role;
+        return this;
+    }
+
+    public Disposition clearRole() {
+        modified &= ~ROLE;
+        role = Role.SENDER;
+        return this;
+    }
+
+    public long getFirst() {
+        return first;
+    }
+
+    public Disposition setFirst(int first) {
+        modified |= FIRST;
+        this.first = Integer.toUnsignedLong(first);
+        return this;
+    }
+
+    public Disposition setFirst(long first) {
+        if (first < 0 || first > UINT_MAX) {
+            throw new IllegalArgumentException("First value given is out of range: " + first);
+        } else {
+            modified |= FIRST;
+        }
+
+        this.first = first;
+        return this;
+    }
+
+    public Disposition clearFirst() {
+        modified &= ~FIRST;
+        first = 0;
+        return this;
+    }
+
+    public long getLast() {
+        return last;
+    }
+
+    public Disposition setLast(int last) {
+        modified |= LAST;
+        this.last = Integer.toUnsignedLong(last);
+        return this;
+    }
+
+    public Disposition setLast(long last) {
+        if (last < 0 || last > UINT_MAX) {
+            throw new IllegalArgumentException("Last value given is out of range: " + last);
+        } else {
+            modified |= LAST;
+        }
+
+        this.last = last;
+        return this;
+    }
+
+    public Disposition clearLast() {
+        modified &= ~LAST;
+        last = 0;
+        return this;
+    }
+
+    public boolean getSettled() {
+        return settled;
+    }
+
+    public Disposition setSettled(boolean settled) {
+        this.modified |= SETTLED;
+        this.settled = settled;
+        return this;
+    }
+
+    public Disposition clearSettled() {
+        modified &= ~SETTLED;
+        settled = false;
+        return this;
+    }
+
+    public DeliveryState getState() {
+        return state;
+    }
+
+    public Disposition setState(DeliveryState state) {
+        if (state != null) {
+            this.modified |= STATE;
+        } else {
+            this.modified &= ~STATE;
+        }
+
+        this.state = state;
+        return this;
+    }
+
+    public Disposition clearState() {
+        modified &= ~STATE;
+        state = null;
+        return this;
+    }
+
+    public boolean getBatchable() {
+        return batchable;
+    }
+
+    public Disposition setBatchable(boolean batchable) {
+        this.modified |= BATCHABLE;
+        this.batchable = batchable;
+        return this;
+    }
+
+    public Disposition clearBatchable() {
+        modified &= ~BATCHABLE;
+        batchable = false;
+        return this;
+    }
+
+    public Disposition reset() {
+        modified = 0;
+        role = Role.SENDER;
+        first = 0;
+        last = 0;
+        settled = false;
+        state = null;
+        batchable = false;
+
+        return this;
+    }
+
+    @Override
+    public Disposition copy() {
+        Disposition copy = new Disposition();
+
+        copy.role = role;
+        copy.first = first;
+        copy.last = last;
+        copy.settled = settled;
+        copy.state = state;
+        copy.batchable = batchable;
+        copy.modified = modified;
+
+        return copy;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.DISPOSITION;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleDisposition(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Disposition{" +
+               "role=" + role +
+               ", first=" + first +
+               ", last=" + last +
+               ", settled=" + settled +
+               ", state=" + state +
+               ", batchable=" + batchable +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/End.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/End.java
new file mode 100644
index 0000000..b37a872
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/End.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class End implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000017L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:end:list");
+
+    private ErrorCondition error;
+
+    public ErrorCondition getError() {
+        return error;
+    }
+
+    public End setError(ErrorCondition error) {
+        this.error = error;
+        return this;
+    }
+
+    @Override
+    public End copy() {
+        End copy = new End();
+        copy.setError(error == null ? null : error.copy());
+        return copy;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.END;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleEnd(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "End{" + "error=" + error + '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ErrorCondition.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ErrorCondition.java
new file mode 100644
index 0000000..acdce6c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ErrorCondition.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class ErrorCondition {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x000000000000001dL);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:error:list");
+
+    private final Symbol condition;
+    private final String description;
+    private final Map<Symbol, Object> info;
+
+    public ErrorCondition(String condition, String description) {
+        this(Symbol.valueOf(condition), description, null);
+    }
+
+    public ErrorCondition(Symbol condition, String description) {
+        this(condition, description, null);
+    }
+
+    public ErrorCondition(Symbol condition, String description, Map<Symbol, Object> info) {
+        this.condition = condition;
+        this.description = description;
+        this.info = info != null ? Collections.unmodifiableMap(info) : null;
+    }
+
+    public Symbol getCondition() {
+        return condition;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public Map<Symbol, Object> getInfo() {
+        return info;
+    }
+
+    public ErrorCondition copy() {
+        return new ErrorCondition(condition, description, info);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        ErrorCondition that = (ErrorCondition) o;
+
+        if (condition != null ? !condition.equals(that.condition) : that.condition != null) {
+            return false;
+        }
+        if (description != null ? !description.equals(that.description) : that.description != null) {
+            return false;
+        }
+        if (info != null ? !info.equals(that.info) : that.info != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = condition != null ? condition.hashCode() : 0;
+        result = 31 * result + (description != null ? description.hashCode() : 0);
+        result = 31 * result + (info != null ? info.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Error{" +
+               "condition=" + condition +
+               ", description='" + description + '\'' +
+               ", info=" + info +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Flow.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Flow.java
new file mode 100644
index 0000000..7b38cdd
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Flow.java
@@ -0,0 +1,454 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Flow implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000013L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:flow:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int NEXT_INCOMING_ID = 1;
+    private static int INCOMING_WINDOW = 2;
+    private static int NEXT_OUTGOING_ID = 4;
+    private static int OUTGOING_WINDOW = 8;
+    private static int HANDLE = 16;
+    private static int DELIVERY_COUNT = 32;
+    private static int LINK_CREDIT = 64;
+    private static int AVAILABLE = 128;
+    private static int DRAIN = 256;
+    private static int ECHO = 512;
+    private static int PROPERTIES = 1024;
+
+    private int modified = 0;
+
+    // TODO - Consider using the matching signed types instead of next largest
+    //        for these values as in most cases we don't actually care about sign.
+    //        In the cases we do care we could just do the math and make these
+    //        interfaces simpler and not check all over the place for overflow.
+
+    private long nextIncomingId;
+    private long incomingWindow;
+    private long nextOutgoingId;
+    private long outgoingWindow;
+    private long handle;
+    private long deliveryCount;
+    private long linkCredit;
+    private long available;
+    private boolean drain;
+    private boolean echo;
+    private Map<Symbol, Object> properties;
+
+    //----- Query the state of the Flow object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasNextIncomingId() {
+        return (modified & NEXT_INCOMING_ID) == NEXT_INCOMING_ID;
+    }
+
+    public boolean hasIncomingWindow() {
+        return (modified & INCOMING_WINDOW) == INCOMING_WINDOW;
+    }
+
+    public boolean hasNextOutgoingId() {
+        return (modified & NEXT_OUTGOING_ID) == NEXT_OUTGOING_ID;
+    }
+
+    public boolean hasOutgoingWindow() {
+        return (modified & OUTGOING_WINDOW) == OUTGOING_WINDOW;
+    }
+
+    public boolean hasHandle() {
+        return (modified & HANDLE) == HANDLE;
+    }
+
+    public boolean hasDeliveryCount() {
+        return (modified & DELIVERY_COUNT) == DELIVERY_COUNT;
+    }
+
+    public boolean hasLinkCredit() {
+        return (modified & LINK_CREDIT) == LINK_CREDIT;
+    }
+
+    public boolean hasAvailable() {
+        return (modified & AVAILABLE) == AVAILABLE;
+    }
+
+    public boolean hasDrain() {
+        return (modified & DRAIN) == DRAIN;
+    }
+
+    public boolean hasEcho() {
+        return (modified & ECHO) == ECHO;
+    }
+
+    public boolean hasProperties() {
+        return (modified & PROPERTIES) == PROPERTIES;
+    }
+
+    //----- Access the AMQP Flow object ------------------------------------//
+
+    public Flow reset() {
+        modified = 0;
+        nextIncomingId = 0;
+        incomingWindow = 0;
+        nextOutgoingId = 0;
+        outgoingWindow = 0;
+        handle = 0;
+        deliveryCount = 0;
+        linkCredit = 0;
+        available = 0;
+        drain = false;
+        echo = false;
+        properties = null;
+
+        return this;
+    }
+
+    public long getNextIncomingId() {
+        return nextIncomingId;
+    }
+
+    public Flow setNextIncomingId(int nextIncomingId) {
+        modified |= NEXT_INCOMING_ID;
+        this.nextIncomingId = Integer.toUnsignedLong(nextIncomingId);
+        return this;
+    }
+
+    public Flow setNextIncomingId(long nextIncomingId) {
+        if (nextIncomingId < 0 || nextIncomingId > UINT_MAX) {
+            throw new IllegalArgumentException("Next Incoming Id value given is out of range: " + nextIncomingId);
+        } else {
+            modified |= NEXT_INCOMING_ID;
+        }
+
+        this.nextIncomingId = nextIncomingId;
+        return this;
+    }
+
+    public Flow clearNextIncomingId() {
+        modified &= ~NEXT_INCOMING_ID;
+        nextIncomingId = 0;
+        return this;
+    }
+
+    public long getIncomingWindow() {
+        return incomingWindow;
+    }
+
+    public Flow setIncomingWindow(int incomingWindow) {
+        modified |= INCOMING_WINDOW;
+        this.incomingWindow = Integer.toUnsignedLong(incomingWindow);
+        return this;
+    }
+
+    public Flow setIncomingWindow(long incomingWindow) {
+        if (incomingWindow < 0 || incomingWindow > UINT_MAX) {
+            throw new IllegalArgumentException("Incoming Window value given is out of range: " + incomingWindow);
+        } else {
+            modified |= INCOMING_WINDOW;
+        }
+
+        this.incomingWindow = incomingWindow;
+        return this;
+    }
+
+    public Flow clearIncomingWindow() {
+        modified &= ~INCOMING_WINDOW;
+        incomingWindow = 0;
+        return this;
+    }
+
+    public long getNextOutgoingId() {
+        return nextOutgoingId;
+    }
+
+    public Flow setNextOutgoingId(int nextOutgoingId) {
+        modified |= NEXT_OUTGOING_ID;
+        this.nextOutgoingId = Integer.toUnsignedLong(nextOutgoingId);
+        return this;
+    }
+
+    public Flow setNextOutgoingId(long nextOutgoingId) {
+        if (nextOutgoingId < 0 || nextOutgoingId > UINT_MAX) {
+            throw new IllegalArgumentException("Next Outgoing Id value given is out of range: " + nextOutgoingId);
+        } else {
+            modified |= NEXT_OUTGOING_ID;
+        }
+
+        this.nextOutgoingId = nextOutgoingId;
+        return this;
+    }
+
+    public Flow clearNextOutgoingId() {
+        modified &= ~NEXT_OUTGOING_ID;
+        nextOutgoingId = 0;
+        return this;
+    }
+
+    public long getOutgoingWindow() {
+        return outgoingWindow;
+    }
+
+    public Flow setOutgoingWindow(int outgoingWindow) {
+        modified |= OUTGOING_WINDOW;
+        this.outgoingWindow = Integer.toUnsignedLong(outgoingWindow);
+        return this;
+    }
+
+    public Flow setOutgoingWindow(long outgoingWindow) {
+        if (outgoingWindow < 0 || outgoingWindow > UINT_MAX) {
+            throw new IllegalArgumentException("Outgoing Window value given is out of range: " + outgoingWindow);
+        } else {
+            modified |= OUTGOING_WINDOW;
+        }
+
+        this.outgoingWindow = outgoingWindow;
+        return this;
+    }
+
+    public Flow clearOutgoingWindow() {
+        modified &= ~OUTGOING_WINDOW;
+        outgoingWindow = 0;
+        return this;
+    }
+
+    public long getHandle() {
+        return handle;
+    }
+
+    public Flow setHandle(int handle) {
+        modified |= HANDLE;
+        this.handle = Integer.toUnsignedLong(handle);
+        return this;
+    }
+
+    public Flow setHandle(long handle) {
+        if (handle < 0 || handle > UINT_MAX) {
+            throw new IllegalArgumentException("Handle value given is out of range: " + handle);
+        } else {
+            modified |= HANDLE;
+        }
+
+        this.handle = handle;
+        return this;
+    }
+
+    public Flow clearHandle() {
+        modified &= ~HANDLE;
+        handle = 0;
+        return this;
+    }
+
+    public long getDeliveryCount() {
+        return deliveryCount;
+    }
+
+    public Flow setDeliveryCount(int deliveryCount) {
+        modified |= DELIVERY_COUNT;
+        this.deliveryCount = Integer.toUnsignedLong(deliveryCount);
+        return this;
+    }
+
+    public Flow setDeliveryCount(long deliveryCount) {
+        if (deliveryCount < 0 || deliveryCount > UINT_MAX) {
+            throw new IllegalArgumentException("Delivery Count value given is out of range: " + deliveryCount);
+        } else {
+            modified |= DELIVERY_COUNT;
+        }
+
+        this.deliveryCount = deliveryCount;
+        return this;
+    }
+
+    public Flow clearDeliveryCount() {
+        modified &= ~DELIVERY_COUNT;
+        deliveryCount = 0;
+        return this;
+    }
+
+    public long getLinkCredit() {
+        return linkCredit;
+    }
+
+    public Flow setLinkCredit(int linkCredit) {
+        modified |= LINK_CREDIT;
+        this.linkCredit = Integer.toUnsignedLong(linkCredit);
+        return this;
+    }
+
+    public Flow setLinkCredit(long linkCredit) {
+        if (linkCredit < 0 || linkCredit > UINT_MAX) {
+            throw new IllegalArgumentException("Link Credit value given is out of range: " + linkCredit);
+        } else {
+            modified |= LINK_CREDIT;
+        }
+
+        this.linkCredit = linkCredit;
+        return this;
+    }
+
+    public Flow clearLinkCredit() {
+        modified &= ~LINK_CREDIT;
+        linkCredit = 0;
+        return this;
+    }
+
+    public long getAvailable() {
+        return available;
+    }
+
+    public Flow setAvailable(int available) {
+        modified |= AVAILABLE;
+        this.available = Integer.toUnsignedLong(available);
+        return this;
+    }
+
+    public Flow setAvailable(long available) {
+        if (available < 0 || available > UINT_MAX) {
+            throw new IllegalArgumentException("Available value given is out of range: " + available);
+        } else {
+            modified |= AVAILABLE;
+        }
+
+        this.available = available;
+        return this;
+    }
+
+    public Flow clearAvailable() {
+        modified &= ~AVAILABLE;
+        available = 0;
+        return this;
+    }
+
+    public boolean getDrain() {
+        return drain;
+    }
+
+    public Flow setDrain(boolean drain) {
+        this.modified |= DRAIN;
+        this.drain = drain;
+        return this;
+    }
+
+    public Flow clearDrain() {
+        modified &= ~DRAIN;
+        drain = false;
+        return this;
+    }
+
+    public boolean getEcho() {
+        return echo;
+    }
+
+    public Flow setEcho(boolean echo) {
+        this.modified |= ECHO;
+        this.echo = echo;
+        return this;
+    }
+
+    public Flow clearEcho() {
+        modified &= ~ECHO;
+        echo = false;
+        return this;
+    }
+
+    public Map<Symbol, Object> getProperties() {
+        return properties;
+    }
+
+    public Flow setProperties(Map<Symbol, Object> properties) {
+        if (properties != null) {
+            modified |= PROPERTIES;
+        } else {
+            modified &= ~PROPERTIES;
+        }
+
+        this.properties = properties;
+        return this;
+    }
+
+    public Flow clearProperties() {
+        modified &= ~PROPERTIES;
+        properties = null;
+        return this;
+    }
+
+    @Override
+    public Flow copy() {
+        Flow copy = new Flow();
+
+        copy.nextIncomingId = nextIncomingId;
+        copy.incomingWindow = incomingWindow;
+        copy.nextOutgoingId = nextOutgoingId;
+        copy.outgoingWindow = outgoingWindow;
+        copy.handle = handle;
+        copy.deliveryCount = deliveryCount;
+        copy.linkCredit = linkCredit;
+        copy.available = available;
+        copy.drain = drain;
+        copy.echo = echo;
+        if (properties != null) {
+            copy.properties = new LinkedHashMap<>(properties);
+        }
+        copy.modified = modified;
+
+        return copy;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.FLOW;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleFlow(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Flow{" +
+               "nextIncomingId=" + nextIncomingId +
+               ", incomingWindow=" + incomingWindow +
+               ", nextOutgoingId=" + nextOutgoingId +
+               ", outgoingWindow=" + outgoingWindow +
+               ", handle=" + handle +
+               ", deliveryCount=" + deliveryCount +
+               ", linkCredit=" + linkCredit +
+               ", available=" + available +
+               ", drain=" + drain +
+               ", echo=" + echo +
+               ", properties=" + properties +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/LinkError.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/LinkError.java
new file mode 100644
index 0000000..f2e31a9
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/LinkError.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public interface LinkError {
+
+    /**
+     * An operator intervened to detach for some reason.
+     */
+    Symbol DETACH_FORCED = Symbol.valueOf("amqp:link:detach-forced");
+
+    /**
+     * The peer sent more message transfers than currently allowed on the link.
+     */
+    Symbol TRANSFER_LIMIT_EXCEEDED = Symbol.valueOf("amqp:link:transfer-limit-exceeded");
+
+    /**
+     * The peer sent a larger message than is supported on the link.
+     */
+    Symbol MESSAGE_SIZE_EXCEEDED = Symbol.valueOf("amqp:link:message-size-exceeded");
+
+    /**
+     * The address provided cannot be resolved to a terminus at the current container. The info map
+     * MAY contain the following information to allow the client to locate the attach to the terminus.
+     * <br>
+     * <ul>
+     *   <li>hostname</li>
+     *     <ul>
+     *       <li>the hostname of the container hosting the terminus. This is the value that SHOULD be
+     *           supplied in the hostname field of the open frame, and during SASL and TLS negotiation
+     *           (if used).
+     *       </li>
+     *     </ul>
+     *   <li>network-host</li>
+     *     <ul>
+     *       <li>the DNS hostname or IP address of the machine hosting the container.</li>
+     *     </ul>
+     *   <li>port</li>
+     *     <ul>
+     *       <li>the port number on the machine hosting the container.</li>
+     *     </ul>
+     *   <li>address</li>
+     *     <ul>
+     *       <li>the address of the terminus at the container.</li>
+     *     </ul>
+     * </ul>
+     */
+    Symbol REDIRECT = Symbol.valueOf("amqp:link:redirect");
+
+    /**
+     * The link has been attached elsewhere, causing the existing attachment to be forcibly closed.
+     */
+    Symbol STOLEN = Symbol.valueOf("amqp:link:stolen");
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Open.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Open.java
new file mode 100644
index 0000000..4275e1d
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Open.java
@@ -0,0 +1,348 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+
+public final class Open implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000010L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:open:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int CONTAINER_ID = 1;
+    private static int HOSTNAME = 2;
+    private static int MAX_FRAME_SIZE = 4;
+    private static int CHANNEL_MAX = 8;
+    private static int IDLE_TIMEOUT = 16;
+    private static int OUTGOING_LOCALES = 32;
+    private static int INCOMING_LOCALES = 64;
+    private static int OFFERED_CAPABILITIES = 128;
+    private static int DESIRED_CAPABILITIES = 256;
+    private static int PROPERTIES = 512;
+
+    private int modified = CONTAINER_ID;
+
+    // TODO - Consider using the matching signed types instead of next largest
+    //        for these values as in most cases we don't actually care about sign.
+    //        In the cases we do care we could just do the math and make these
+    //        interfaces simpler and not check all over the place for overflow.
+
+    private String containerId = "";
+    private String hostname;
+    private long maxFrameSize = UnsignedInteger.MAX_VALUE.longValue();
+    private int channelMax = UnsignedShort.MAX_VALUE.intValue();
+    private long idleTimeout;
+    private Symbol[] outgoingLocales;
+    private Symbol[] incomingLocales;
+    private Symbol[] offeredCapabilities;
+    private Symbol[] desiredCapabilities;
+    private Map<Symbol, Object> properties;
+
+    @Override
+    public Open copy() {
+        Open copy = new Open();
+
+        copy.setContainerId(containerId);
+        if (hasHostname()) {
+            copy.setHostname(hostname);
+        }
+        if (hasMaxFrameSize()) {
+            copy.setMaxFrameSize(maxFrameSize);
+        }
+        if (hasChannelMax()) {
+            copy.setChannelMax(channelMax);
+        }
+        if (hasIdleTimeout()) {
+            copy.setIdleTimeout(idleTimeout);
+        }
+        if (hasOutgoingLocales()) {
+            copy.setOutgoingLocales(Arrays.copyOf(outgoingLocales, outgoingLocales.length));
+        }
+        if (hasIncomingLocales()) {
+            copy.setIncomingLocales(Arrays.copyOf(incomingLocales, incomingLocales.length));
+        }
+        if (hasOfferedCapabilites()) {
+            copy.setOfferedCapabilities(Arrays.copyOf(offeredCapabilities, offeredCapabilities.length));
+        }
+        if (hasDesiredCapabilites()) {
+            copy.setOfferedCapabilities(Arrays.copyOf(desiredCapabilities, desiredCapabilities.length));
+        }
+        if (hasProperties()) {
+            copy.setProperties(new LinkedHashMap<>(properties));
+        }
+
+        return copy;
+    }
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasContainerId() {
+        return (modified & CONTAINER_ID) == CONTAINER_ID;
+    }
+
+    public boolean hasHostname() {
+        return (modified & HOSTNAME) == HOSTNAME;
+    }
+
+    public boolean hasMaxFrameSize() {
+        return (modified & MAX_FRAME_SIZE) == MAX_FRAME_SIZE;
+    }
+
+    public boolean hasChannelMax() {
+        return (modified & CHANNEL_MAX) == CHANNEL_MAX;
+    }
+
+    public boolean hasIdleTimeout() {
+        return (modified & IDLE_TIMEOUT) == IDLE_TIMEOUT;
+    }
+
+    public boolean hasOutgoingLocales() {
+        return (modified & OUTGOING_LOCALES) == OUTGOING_LOCALES;
+    }
+
+    public boolean hasIncomingLocales() {
+        return (modified & INCOMING_LOCALES) == INCOMING_LOCALES;
+    }
+
+    public boolean hasOfferedCapabilites() {
+        return (modified & OFFERED_CAPABILITIES) == OFFERED_CAPABILITIES;
+    }
+
+    public boolean hasDesiredCapabilites() {
+        return (modified & DESIRED_CAPABILITIES) == DESIRED_CAPABILITIES;
+    }
+
+    public boolean hasProperties() {
+        return (modified & PROPERTIES) == PROPERTIES;
+    }
+
+    //----- Access to the member data with state checks
+
+    public String getContainerId() {
+        return containerId;
+    }
+
+    public Open setContainerId(String containerId) {
+        if (containerId == null) {
+            throw new NullPointerException("the container-id field is mandatory");
+        }
+
+        modified |= CONTAINER_ID;
+
+        this.containerId = containerId;
+        return this;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public Open setHostname(String hostname) {
+        if (hostname == null) {
+            modified &= ~HOSTNAME;
+        } else {
+            modified |= HOSTNAME;
+        }
+
+        this.hostname = hostname;
+        return this;
+    }
+
+    public long getMaxFrameSize() {
+        return maxFrameSize;
+    }
+
+    public Open setMaxFrameSize(int maxFrameSize) {
+        modified |= MAX_FRAME_SIZE;
+        this.maxFrameSize = Integer.toUnsignedLong(maxFrameSize);
+        return this;
+    }
+
+    public Open setMaxFrameSize(long maxFrameSize) {
+        if (maxFrameSize < 0 || maxFrameSize > UINT_MAX) {
+            throw new IllegalArgumentException(String.format(
+                "Given max frame size value %d larger than this implementations limit of %d",
+                maxFrameSize, Integer.MAX_VALUE));
+        } else {
+            modified |= MAX_FRAME_SIZE;
+        }
+
+        this.maxFrameSize = maxFrameSize;
+        return this;
+    }
+
+    public int getChannelMax() {
+        return channelMax;
+    }
+
+    public Open setChannelMax(short channelMax) {
+        modified |= CHANNEL_MAX;
+        this.channelMax = Short.toUnsignedInt(channelMax);
+        return this;
+    }
+
+    public Open setChannelMax(int channelMax) {
+        if (channelMax < 0 || channelMax > UnsignedShort.MAX_VALUE.intValue()) {
+            throw new IllegalArgumentException("The Channel Max value given is out of range: " + channelMax);
+        } else {
+            modified |= CHANNEL_MAX;
+        }
+
+        this.channelMax = channelMax;
+        return this;
+    }
+
+    public long getIdleTimeout() {
+        return idleTimeout;
+    }
+
+    public Open setIdleTimeout(int idleTimeOut) {
+        modified |= IDLE_TIMEOUT;
+        this.idleTimeout = Integer.toUnsignedLong(idleTimeOut);
+        return this;
+    }
+
+    public Open setIdleTimeout(long idleTimeOut) {
+        if (idleTimeOut < 0 || idleTimeOut > UINT_MAX) {
+            throw new IllegalArgumentException("The Idle Timeout value given is out of range: " + idleTimeOut);
+        } else {
+            modified |= IDLE_TIMEOUT;
+        }
+
+        this.idleTimeout = idleTimeOut;
+        return this;
+    }
+
+    public Symbol[] getOutgoingLocales() {
+        return outgoingLocales;
+    }
+
+    public Open setOutgoingLocales(Symbol... outgoingLocales) {
+        if (outgoingLocales != null) {
+            modified |= OUTGOING_LOCALES;
+        } else {
+            modified &= ~OUTGOING_LOCALES;
+        }
+
+        this.outgoingLocales = outgoingLocales;
+        return this;
+    }
+
+    public Symbol[] getIncomingLocales() {
+        return incomingLocales;
+    }
+
+    public Open setIncomingLocales(Symbol... incomingLocales) {
+        if (incomingLocales != null) {
+            modified |= INCOMING_LOCALES;
+        } else {
+            modified &= ~INCOMING_LOCALES;
+        }
+
+        this.incomingLocales = incomingLocales;
+        return this;
+    }
+
+    public Symbol[] getOfferedCapabilities() {
+        return offeredCapabilities;
+    }
+
+    public Open setOfferedCapabilities(Symbol... offeredCapabilities) {
+        if (offeredCapabilities != null) {
+            modified |= OFFERED_CAPABILITIES;
+        } else {
+            modified &= ~OFFERED_CAPABILITIES;
+        }
+
+        this.offeredCapabilities = offeredCapabilities;
+        return this;
+    }
+
+    public Symbol[] getDesiredCapabilities() {
+        return desiredCapabilities;
+    }
+
+    public Open setDesiredCapabilities(Symbol... desiredCapabilities) {
+        if (desiredCapabilities != null) {
+            modified |= DESIRED_CAPABILITIES;
+        } else {
+            modified &= ~DESIRED_CAPABILITIES;
+        }
+
+        this.desiredCapabilities = desiredCapabilities;
+        return this;
+    }
+
+    public Map<Symbol, Object> getProperties() {
+        return properties;
+    }
+
+    public Open setProperties(Map<Symbol, Object> properties) {
+        if (properties != null) {
+            modified |= PROPERTIES;
+        } else {
+            modified &= ~PROPERTIES;
+        }
+
+        this.properties = properties;
+        return this;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.OPEN;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleOpen(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Open{" +
+               " containerId='" + containerId + '\'' +
+               ", hostname='" + hostname + '\'' +
+               ", maxFrameSize=" + maxFrameSize +
+               ", channelMax=" + channelMax +
+               ", idleTimeOut=" + idleTimeout +
+               ", outgoingLocales=" + (outgoingLocales == null ? null : Arrays.asList(outgoingLocales)) +
+               ", incomingLocales=" + (incomingLocales == null ? null : Arrays.asList(incomingLocales)) +
+               ", offeredCapabilities=" + (offeredCapabilities == null ? null : Arrays.asList(offeredCapabilities)) +
+               ", desiredCapabilities=" + (desiredCapabilities == null ? null : Arrays.asList(desiredCapabilities)) +
+               ", properties=" + properties +
+               '}';
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Performative.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Performative.java
new file mode 100644
index 0000000..25d6ba4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Performative.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+
+/**
+ * Marker interface for AMQP Performatives
+ */
+public interface Performative {
+
+    enum PerformativeType {
+        ATTACH,
+        BEGIN,
+        CLOSE,
+        DETACH,
+        DISPOSITION,
+        END,
+        FLOW,
+        OPEN,
+        TRANSFER
+    }
+
+    PerformativeType getPerformativeType();
+
+    Performative copy();
+
+    interface PerformativeHandler<E> {
+
+        default void handleOpen(Open open, ProtonBuffer payload, int channel, E context) {}
+        default void handleBegin(Begin begin, ProtonBuffer payload, int channel, E context) {}
+        default void handleAttach(Attach attach, ProtonBuffer payload, int channel, E context) {}
+        default void handleFlow(Flow flow, ProtonBuffer payload, int channel, E context) {}
+        default void handleTransfer(Transfer transfer, ProtonBuffer payload, int channel, E context) {}
+        default void handleDisposition(Disposition disposition, ProtonBuffer payload, int channel, E context) {}
+        default void handleDetach(Detach detach, ProtonBuffer payload, int channel, E context) {}
+        default void handleEnd(End end, ProtonBuffer payload, int channel, E context) {}
+        default void handleClose(Close close, ProtonBuffer payload, int channel, E context) {}
+
+    }
+
+    <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context);
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ReceiverSettleMode.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ReceiverSettleMode.java
new file mode 100644
index 0000000..edd5de4
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/ReceiverSettleMode.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.types.UnsignedByte;
+
+public enum ReceiverSettleMode {
+
+    FIRST(0), SECOND(1);
+
+    private UnsignedByte value;
+
+    private ReceiverSettleMode(int value) {
+        this.value = UnsignedByte.valueOf((byte)value);
+    }
+
+    public static ReceiverSettleMode valueOf(UnsignedByte value) {
+        return value == null ? FIRST : ReceiverSettleMode.valueOf(value.byteValue());
+    }
+
+    public static ReceiverSettleMode valueOf(byte value) {
+        switch (value) {
+            case 0:
+                return ReceiverSettleMode.FIRST;
+            case 1:
+                return ReceiverSettleMode.SECOND;
+            default:
+                throw new IllegalArgumentException("The value can be only 0 (for FIRST) and 1 (for SECOND)");
+        }
+    }
+
+    public byte byteValue() {
+        return value.byteValue();
+    }
+
+    public UnsignedByte getValue() {
+        return value;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Role.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Role.java
new file mode 100644
index 0000000..9e5a13c
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Role.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+public enum Role {
+
+    SENDER(false), RECEIVER(true);
+
+    private final boolean receiver;
+
+    private Role(boolean receiver) {
+        this.receiver = receiver;
+    }
+
+    public boolean getValue() {
+        return receiver;
+    }
+
+    public static Role valueOf(boolean role) {
+        if (role) {
+            return RECEIVER;
+        } else {
+            return SENDER;
+        }
+    }
+
+    public static Role valueOf(Boolean role) {
+        if (Boolean.TRUE.equals(role)) {
+            return RECEIVER;
+        } else {
+            return SENDER;
+        }
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/SenderSettleMode.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/SenderSettleMode.java
new file mode 100644
index 0000000..98739aa
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/SenderSettleMode.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.types.UnsignedByte;
+
+public enum SenderSettleMode {
+
+    UNSETTLED(0), SETTLED(1), MIXED(2);
+
+    private UnsignedByte value;
+
+    private SenderSettleMode(int value) {
+        this.value = UnsignedByte.valueOf((byte)value);
+    }
+
+    public static SenderSettleMode valueOf(UnsignedByte value) {
+        return value == null ? MIXED : SenderSettleMode.valueOf(value.byteValue());
+    }
+
+    public static SenderSettleMode valueOf(byte value) {
+        switch (value) {
+            case 0:
+                return SenderSettleMode.UNSETTLED;
+            case 1:
+                return SenderSettleMode.SETTLED;
+            case 2:
+                return SenderSettleMode.MIXED;
+            default:
+                throw new IllegalArgumentException("The value can be only 0 (for UNSETTLED), 1 (for SETTLED) and 2 (for MIXED)");
+        }
+    }
+
+    public byte byteValue() {
+        return value.byteValue();
+    }
+
+    public UnsignedByte getValue() {
+        return value;
+    }
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/SessionError.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/SessionError.java
new file mode 100644
index 0000000..b6e73f7
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/SessionError.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.types.Symbol;
+
+public interface SessionError {
+
+    /**
+     * The peer violated incoming window for the session.
+     */
+    Symbol WINDOW_VIOLATION = Symbol.valueOf("amqp:session:window-violation");
+
+    /**
+     * Input was received for a link that was detached with an error.
+     */
+    Symbol ERRANT_LINK = Symbol.valueOf("amqp:session:errant-link");
+
+    /**
+     * An attach was received using a handle that is already in use for an attached link.
+     */
+    Symbol HANDLE_IN_USE = Symbol.valueOf("amqp:session:handle-in-use");
+
+    /**
+     * A frame (other than attach) was received referencing a handle which is not currently in use of an attached link.
+     */
+    Symbol UNATTACHED_HANDLE = Symbol.valueOf("amqp:session:unattached-handle");
+
+}
diff --git a/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Transfer.java b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Transfer.java
new file mode 100644
index 0000000..516ff20
--- /dev/null
+++ b/protonj2/src/main/java/org/apache/qpid/protonj2/types/transport/Transfer.java
@@ -0,0 +1,422 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public final class Transfer implements Performative {
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000000000000014L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("amqp:transfer:list");
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+
+    private static int HANDLE = 1;
+    private static int DELIVERY_ID = 2;
+    private static int DELIVERY_TAG = 4;
+    private static int MESSAGE_FORMAT = 8;
+    private static int SETTLED = 16;
+    private static int MORE = 32;
+    private static int RCV_SETTLE_MODE = 64;
+    private static int STATE = 128;
+    private static int RESUME = 256;
+    private static int ABORTED = 512;
+    private static int BATCHABLE = 1024;
+
+    private int modified = 0;
+
+    // TODO - Consider using the matching signed types instead of next largest
+    //        for these values as in most cases we don't actually care about sign.
+    //        In the cases we do care we could just do the math and make these
+    //        interfaces simpler and not check all over the place for overflow.
+
+    private long handle;
+    private long deliveryId;
+    private DeliveryTag deliveryTag;
+    private long messageFormat;
+    private boolean settled;
+    private boolean more;
+    private ReceiverSettleMode rcvSettleMode;
+    private DeliveryState state;
+    private boolean resume;
+    private boolean aborted;
+    private boolean batchable;
+
+    //----- Query the state of the Header object -----------------------------//
+
+    public boolean isEmpty() {
+        return modified == 0;
+    }
+
+    public int getElementCount() {
+        return 32 - Integer.numberOfLeadingZeros(modified);
+    }
+
+    public boolean hasHandle() {
+        return (modified & HANDLE) == HANDLE;
+    }
+
+    public boolean hasDeliveryId() {
+        return (modified & DELIVERY_ID) == DELIVERY_ID;
+    }
+
+    public boolean hasDeliveryTag() {
+        return (modified & DELIVERY_TAG) == DELIVERY_TAG;
+    }
+
+    public boolean hasMessageFormat() {
+        return (modified & MESSAGE_FORMAT) == MESSAGE_FORMAT;
+    }
+
+    public boolean hasSettled() {
+        return (modified & SETTLED) == SETTLED;
+    }
+
+    public boolean hasMore() {
+        return (modified & MORE) == MORE;
+    }
+
+    public boolean hasRcvSettleMode() {
+        return (modified & RCV_SETTLE_MODE) == RCV_SETTLE_MODE;
+    }
+
+    public boolean hasState() {
+        return (modified & STATE) == STATE;
+    }
+
+    public boolean hasResume() {
+        return (modified & RESUME) == RESUME;
+    }
+
+    public boolean hasAborted() {
+        return (modified & ABORTED) == ABORTED;
+    }
+
+    public boolean hasBatchable() {
+        return (modified & BATCHABLE) == BATCHABLE;
+    }
+
+    //----- Access the AMQP Transfer object ------------------------------------//
+
+    public long getHandle() {
+        return handle;
+    }
+
+    public Transfer setHandle(int handle) {
+        modified |= HANDLE;
+        this.handle = Integer.toUnsignedLong(handle);
+        return this;
+    }
+
+    public Transfer setHandle(long handle) {
+        if (handle < 0 || handle > UINT_MAX) {
+            throw new IllegalArgumentException("Handle value given is out of range: " + handle);
+        } else {
+            modified |= HANDLE;
+        }
+
+        this.handle = handle;
+        return this;
+    }
+
+    public Transfer clearHandle() {
+        modified &= ~HANDLE;
+        handle = 0;
+        return this;
+    }
+
+    public long getDeliveryId() {
+        return deliveryId;
+    }
+
+    public Transfer setDeliveryId(int deliveryId) {
+        modified |= DELIVERY_ID;
+        this.deliveryId = Integer.toUnsignedLong(deliveryId);
+        return this;
+    }
+
+    public Transfer setDeliveryId(long deliveryId) {
+        if (deliveryId < 0 || deliveryId > UINT_MAX) {
+            throw new IllegalArgumentException("Delivery ID value given is out of range: " + deliveryId);
+        } else {
+            modified |= DELIVERY_ID;
+        }
+
+        this.deliveryId = deliveryId;
+        return this;
+    }
+
+    public Transfer clearDeliveryId() {
+        modified &= ~DELIVERY_ID;
+        deliveryId = 0;
+        return this;
+    }
+
+    public DeliveryTag getDeliveryTag() {
+        return deliveryTag;
+    }
+
+    public Transfer setDeliveryTag(byte[] tagBytes) {
+        if (tagBytes != null) {
+            return setDeliveryTag(new DeliveryTag.ProtonDeliveryTag(ProtonByteBufferAllocator.DEFAULT.wrap(tagBytes)));
+        } else {
+            return setDeliveryTag((DeliveryTag) null);
+        }
+    }
+
+    public Transfer setDeliveryTag(ProtonBuffer tagBytes) {
+        if (tagBytes != null) {
+            return setDeliveryTag(new DeliveryTag.ProtonDeliveryTag(tagBytes));
+        } else {
+            return setDeliveryTag((DeliveryTag) null);
+        }
+    }
+
+    public Transfer setDeliveryTag(DeliveryTag deliveryTag) {
+        if (deliveryTag != null) {
+            modified |= DELIVERY_TAG;
+        } else {
+            modified &= ~DELIVERY_TAG;
+        }
+
+        this.deliveryTag = deliveryTag;
+        return this;
+    }
+
+    public Transfer clearDeliveryTag() {
+        modified &= ~DELIVERY_TAG;
+        deliveryTag = null;
+        return this;
+    }
+
+    public long getMessageFormat() {
+        return messageFormat;
+    }
+
+    public Transfer setMessageFormat(int messageFormat) {
+        modified |= MESSAGE_FORMAT;
+        this.messageFormat = Integer.toUnsignedLong(messageFormat);
+        return this;
+    }
+
+    public Transfer setMessageFormat(long messageFormat) {
+        if (messageFormat < 0 || messageFormat > UINT_MAX) {
+            throw new IllegalArgumentException("Message Format value given is out of range: " + messageFormat);
+        } else {
+            modified |= MESSAGE_FORMAT;
+        }
+
+        this.messageFormat = messageFormat;
+        return this;
+    }
+
+    public Transfer clearMessageFormat() {
+        modified &= ~MESSAGE_FORMAT;
+        messageFormat = 0;
+        return this;
+    }
+
+    public boolean getSettled() {
+        return settled;
+    }
+
+    public Transfer setSettled(boolean settled) {
+        this.modified |= SETTLED;
+        this.settled = settled;
+        return this;
+    }
+
+    public Transfer clearSettled() {
+        modified &= ~SETTLED;
+        settled = false;
+        return this;
+    }
+
+    public boolean getMore() {
+        return more;
+    }
+
+    public Transfer setMore(boolean more) {
+        this.modified |= MORE;
+        this.more = more;
+        return this;
+    }
+
+    public Transfer clearMore() {
+        modified &= ~MORE;
+        more = false;
+        return this;
+    }
+
+    public ReceiverSettleMode getRcvSettleMode() {
+        return rcvSettleMode;
+    }
+
+    public Transfer setRcvSettleMode(ReceiverSettleMode rcvSettleMode) {
+        if (rcvSettleMode != null) {
+            modified |= RCV_SETTLE_MODE;
+        } else {
+            modified &= ~RCV_SETTLE_MODE;
+        }
+
+        this.rcvSettleMode = rcvSettleMode;
+        return this;
+    }
+
+    public Transfer clearRcvSettleMode() {
+        modified &= ~RCV_SETTLE_MODE;
+        rcvSettleMode = null;
+        return this;
+    }
+
+    public DeliveryState getState() {
+        return state;
+    }
+
+    public Transfer setState(DeliveryState state) {
+        if (state != null) {
+            modified |= STATE;
+        } else {
+            modified &= ~STATE;
+        }
+
+        this.state = state;
+        return this;
+    }
+
+    public Transfer clearState() {
+        modified &= ~STATE;
+        state = null;
+        return this;
+    }
+
+    public boolean getResume() {
+        return resume;
+    }
+
+    public Transfer setResume(boolean resume) {
+        this.modified |= RESUME;
+        this.resume = resume;
+        return this;
+    }
+
+    public Transfer clearResume() {
+        modified &= ~RESUME;
+        resume = false;
+        return this;
+    }
+
+    public boolean getAborted() {
+        return aborted;
+    }
+
+    public Transfer setAborted(boolean aborted) {
+        this.modified |= ABORTED;
+        this.aborted = aborted;
+        return this;
+    }
+
+    public Transfer clearAborted() {
+        modified &= ~ABORTED;
+        aborted = false;
+        return this;
+    }
+
+    public boolean getBatchable() {
+        return batchable;
+    }
+
+    public Transfer setBatchable(boolean batchable) {
+        this.modified |= BATCHABLE;
+        this.batchable = batchable;
+        return this;
+    }
+
+    public Transfer clearBatchable() {
+        modified &= ~BATCHABLE;
+        batchable = false;
+        return this;
+    }
+
+    public Transfer reset() {
+        modified = 0;
+        handle = 0;
+        deliveryId = 0;
+        deliveryTag = null;
+        messageFormat = 0;
+        settled = false;
+        more = false;
+        rcvSettleMode = null;
+        state = null;
+        resume = false;
+        aborted = false;
+        batchable = false;
+
+        return this;
+    }
+
+    @Override
+    public Transfer copy() {
+        Transfer copy = new Transfer();
+
+        copy.handle = handle;
+        copy.deliveryId = deliveryId;
+        copy.deliveryTag = deliveryTag == null ? null : deliveryTag.copy();
+        copy.messageFormat = messageFormat;
+        copy.settled = settled;
+        copy.more = more;
+        copy.rcvSettleMode = rcvSettleMode;
+        copy.state = state;
+        copy.resume = resume;
+        copy.aborted = aborted;
+        copy.batchable = batchable;
+        copy.modified = modified;
+
+        return copy;
+    }
+
+    @Override
+    public PerformativeType getPerformativeType() {
+        return PerformativeType.TRANSFER;
+    }
+
+    @Override
+    public <E> void invoke(PerformativeHandler<E> handler, ProtonBuffer payload, int channel, E context) {
+        handler.handleTransfer(this, payload, channel, context);
+    }
+
+    @Override
+    public String toString() {
+        return "Transfer{" +
+               "handle=" + (hasHandle() ? handle : "null") +
+               ", deliveryId=" + (hasDeliveryId() ? deliveryId : "null") +
+               ", deliveryTag=" + (hasDeliveryTag() ? deliveryTag : "null") +
+               ", messageFormat=" + (hasMessageFormat() ? messageFormat : "null") +
+               ", settled=" + (hasSettled() ? settled : "null") +
+               ", more=" + (hasMore() ? more : "null") +
+               ", rcvSettleMode=" + (hasRcvSettleMode() ? rcvSettleMode : "null") +
+               ", state=" + (hasState() ? state : "null") +
+               ", resume=" + (hasResume() ? resume : "null") +
+               ", aborted=" + (hasAborted() ? aborted : "null") +
+               ", batchable=" + (hasBatchable() ? batchable : "null") +
+               '}';
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonAbstractBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonAbstractBufferTest.java
new file mode 100644
index 0000000..8842e2a
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonAbstractBufferTest.java
@@ -0,0 +1,3335 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Abstract test base for testing common expected behaviors of ProtonBuffer implementations
+ * of ProtonBuffer.
+ */
+public abstract class ProtonAbstractBufferTest {
+
+    public static final int LARGE_CAPACITY = 4096; // Must be even for these tests
+    public static final int BLOCK_SIZE = 128;
+    public static final int DEFAULT_CAPACITY = 64;
+
+    protected long seed;
+    protected Random random;
+
+    @BeforeEach
+    public void setUp() {
+        seed = System.currentTimeMillis();
+        random = new Random();
+        random.setSeed(seed);
+    }
+
+    //----- Test Buffer creation ---------------------------------------------//
+
+    @Test
+    public void testConstructWithDifferingCapacityAndMaxCapacity() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        final int baseCapaity = DEFAULT_CAPACITY + 10;
+
+        ProtonBuffer buffer = allocateBuffer(baseCapaity, baseCapaity + 100);
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(baseCapaity, buffer.capacity());
+        assertEquals(baseCapaity + 100, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testBufferRespectsMaxCapacityAfterGrowingToFit() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(5, 10);
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(5, buffer.capacity());
+        assertEquals(10, buffer.maxCapacity());
+
+        for (int i = 0; i < 10; ++i) {
+            buffer.writeByte(i);
+        }
+
+        try {
+            buffer.writeByte(10);
+            fail("Should not be able to write more than the max capacity bytes");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    @Test
+    public void testBufferRespectsMaxCapacityLimitNoGrowthScenario() {
+        ProtonBuffer buffer = allocateBuffer(10, 10);
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(10, buffer.capacity());
+        assertEquals(10, buffer.maxCapacity());
+
+        // Writes to capacity work, but exceeding that should fail.
+        for (int i = 0; i < 10; ++i) {
+            buffer.writeByte(i);
+        }
+
+        try {
+            buffer.writeByte(10);
+            fail("Should not be able to write more than the max capacity bytes");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    //----- Tests for altering buffer capacity -------------------------------//
+
+    @Test
+    public void testCapacityEnforceMaxCapacity() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(3, 13);
+        assertEquals(13, buffer.maxCapacity());
+        assertEquals(3, buffer.capacity());
+
+        assertThrows(IllegalArgumentException.class, () -> buffer.capacity(14));
+    }
+
+    @Test
+    public void testCapacityNegative() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(3, 13);
+        assertEquals(13, buffer.maxCapacity());
+        assertEquals(3, buffer.capacity());
+
+        assertThrows(IllegalArgumentException.class, () -> buffer.capacity(-1));
+    }
+
+    @Test
+    public void testCapacityDecrease() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(3, 13);
+        assertEquals(13, buffer.maxCapacity());
+        assertEquals(3, buffer.capacity());
+        buffer.capacity(2);
+        assertEquals(2, buffer.capacity());
+        assertEquals(13, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testCapacityIncrease() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(3, 13);
+        assertEquals(13, buffer.maxCapacity());
+        assertEquals(3, buffer.capacity());
+        buffer.capacity(4);
+        assertEquals(4, buffer.capacity());
+        assertEquals(13, buffer.maxCapacity());
+    }
+
+    //----- Tests for altering buffer properties -----------------------------//
+
+    @Test
+    public void testSetReadIndexWithNegative() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setReadIndex(-1);
+            fail("Should not accept negative values");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetReadIndexGreaterThanCapacity() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setReadIndex(buffer.capacity() + buffer.capacity());
+            fail("Should not accept values bigger than capacity");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetWriteIndexWithNegative() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setWriteIndex(-1);
+            fail("Should not accept negative values");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetWriteIndexGreaterThanCapacity() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setWriteIndex(buffer.capacity() + buffer.capacity());
+            fail("Should not accept values bigger than capacity");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetIndexWithNegativeReadIndex() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setIndex(-1, 0);
+            fail("Should not accept negative values");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetIndexWithNegativeWriteIndex() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setIndex(0, -1);
+            fail("Should not accept negative values");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetIndexWithReadIndexBiggerThanWrite() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setIndex(50, 40);
+            fail("Should not accept bigger read index values");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testSetIndexWithWriteIndexBiggerThanCapacity() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        try {
+            buffer.setIndex(0, buffer.capacity() + 1);
+            fail("Should not accept write index bigger than capacity");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testIsReadable() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        assertFalse(buffer.isReadable());
+        buffer.writeBoolean(false);
+        assertTrue(buffer.isReadable());
+    }
+
+    @Test
+    public void testIsReadableWithAmount() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        assertFalse(buffer.isReadable(1));
+        buffer.writeBoolean(false);
+        assertTrue(buffer.isReadable(1));
+        assertFalse(buffer.isReadable(2));
+    }
+
+    @Test
+    public void testIsWriteable() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        assertTrue(buffer.isWritable());
+        buffer.setWriteIndex(buffer.capacity());
+        assertFalse(buffer.isWritable());
+    }
+
+    @Test
+    public void testIsWriteableWithAmount() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        assertTrue(buffer.isWritable());
+        buffer.setWriteIndex(buffer.capacity() - 1);
+        assertTrue(buffer.isWritable(1));
+        assertFalse(buffer.isWritable(2));
+    }
+
+    @Test
+    public void testMaxWritableBytes() {
+        ProtonBuffer buffer = allocateBuffer(DEFAULT_CAPACITY, DEFAULT_CAPACITY);
+        assertTrue(buffer.isWritable());
+        assertEquals(DEFAULT_CAPACITY, buffer.getMaxWritableBytes());
+        buffer.setWriteIndex(buffer.capacity() - 1);
+        assertTrue(buffer.isWritable(1));
+        assertFalse(buffer.isWritable(2));
+        assertEquals(1, buffer.getMaxWritableBytes());
+    }
+
+    @Test
+    public void testClear() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(0, buffer.getWriteIndex());
+        buffer.setIndex(10, 20);
+        assertEquals(10, buffer.getReadIndex());
+        assertEquals(20, buffer.getWriteIndex());
+        buffer.clear();
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(0, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testSkipBytes() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        buffer.setWriteIndex(buffer.capacity() / 2);
+        assertEquals(0, buffer.getReadIndex());
+        buffer.skipBytes(buffer.capacity() / 2);
+        assertEquals(buffer.capacity() / 2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testSkipBytesBeyondReable() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        buffer.setWriteIndex(buffer.capacity() / 2);
+        assertEquals(0, buffer.getReadIndex());
+
+        try {
+            buffer.skipBytes(buffer.getReadableBytes() + 50);
+            fail("Should not be able to skip beyond write index");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    //----- Tests for altering buffer capacity -------------------------------//
+
+    @Test
+    public void testIncreaseCapacity() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = allocateBuffer(100).writeBytes(source);
+        assertEquals(100, buffer.capacity());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(100, buffer.getWriteIndex());
+
+        buffer.capacity(200);
+        assertEquals(200, buffer.capacity());
+
+        buffer.capacity(200);
+        assertEquals(200, buffer.capacity());
+
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(100, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testDecreaseCapacity() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = allocateBuffer(100).writeBytes(source);
+        assertEquals(100, buffer.capacity());
+        assertEquals(100, buffer.getWriteIndex());
+
+        buffer.capacity(50);
+        assertEquals(50, buffer.capacity());
+
+        // Buffer is truncated but we never read anything so read index stays at front.
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(50, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testDecreaseCapacityValidatesSize() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(100);
+        assertEquals(100, buffer.capacity());
+
+        try {
+            buffer.capacity(-50);
+            fail("Should throw IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testDecreaseCapacityWithReadIndexIndexBeyondNewValue() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = allocateBuffer(100).writeBytes(source);
+        assertEquals(100, buffer.capacity());
+
+        buffer.setReadIndex(60);
+
+        buffer.capacity(50);
+        assertEquals(50, buffer.capacity());
+
+        // Buffer should be truncated and read index moves back to end
+        assertEquals(50, buffer.getReadIndex());
+        assertEquals(50, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testDecreaseCapacityWithWriteIndexWithinNewValue() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = allocateBuffer(100).writeBytes(source);
+        assertEquals(100, buffer.capacity());
+
+        buffer.setIndex(10, 30);
+        buffer.capacity(50);
+        assertEquals(50, buffer.capacity());
+
+        // Buffer should be truncated but index values remain unchanged
+        assertEquals(10, buffer.getReadIndex());
+        assertEquals(30, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testCapacityIncreasesWhenWritesExceedCurrent() {
+        assumeTrue(canBufferCapacityBeChanged());
+
+        ProtonBuffer buffer = allocateBuffer(10);
+
+        assertTrue(buffer.hasArray());
+
+        assertEquals(10, buffer.capacity());
+        assertEquals(Integer.MAX_VALUE, buffer.maxCapacity());
+
+        for (int i = 1; i <= 9; ++i) {
+            buffer.writeByte(i);
+        }
+
+        assertEquals(10, buffer.capacity());
+
+        buffer.writeByte(10);
+
+        assertEquals(10, buffer.capacity());
+
+        buffer.writeByte(11);
+
+        assertTrue(buffer.capacity() > 10);
+
+        assertEquals(11, buffer.getReadableBytes());
+
+        for (int i = 1; i < 12; ++i) {
+            assertEquals(i, buffer.readByte());
+        }
+    }
+
+    //----- Write Bytes Tests ------------------------------------------------//
+
+    @Test
+    public void testWriteBytes() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(payload);
+
+        assertEquals(payload.length, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.readByte());
+        }
+
+        assertEquals(payload.length, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesWithEmptyArray() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(new byte[0]);
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesNPEWhenNullGiven() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        try {
+            buffer.writeBytes((byte[]) null);
+            fail();
+        } catch (NullPointerException ex) {}
+
+        try {
+            buffer.writeBytes((byte[]) null, 0);
+            fail();
+        } catch (NullPointerException ex) {}
+
+        try {
+            buffer.writeBytes((byte[]) null, 0, 0);
+            fail();
+        } catch (NullPointerException ex) {}
+    }
+
+    @Test
+    public void testWriteBytesWithLength() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(payload, payload.length);
+
+        assertEquals(payload.length, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.readByte());
+        }
+
+        assertEquals(payload.length, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesWithLengthToBig() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        try {
+            buffer.writeBytes(payload, payload.length + 1);
+            fail("Should not write when length given is to large.");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testWriteBytesWithNegativeLength() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        try {
+            buffer.writeBytes(payload, -1);
+            fail("Should not write when length given is negative.");
+        } catch (IllegalArgumentException ex) {}
+    }
+
+    @Test
+    public void testWriteBytesWithLengthAndOffset() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(payload, 0, payload.length);
+
+        assertEquals(payload.length, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.readByte());
+        }
+
+        assertEquals(payload.length, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesWithLengthAndOffsetIncorrect() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        try {
+            buffer.writeBytes(payload, 0, payload.length + 1);
+            fail("Should not write when length given is to large.");
+        } catch (IndexOutOfBoundsException ex) {}
+
+        try {
+            buffer.writeBytes(payload, -1, payload.length);
+            fail("Should not write when offset given is negative.");
+        } catch (IndexOutOfBoundsException ex) {}
+
+        try {
+            buffer.writeBytes(payload, 0, -1);
+            fail("Should not write when length given is negative.");
+        } catch (IllegalArgumentException ex) {}
+
+        try {
+            buffer.writeBytes(payload, payload.length + 1, 1);
+            fail("Should not write when offset given is to large.");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testWriteBytesFromProtonBuffer() {
+        ProtonBuffer source = new ProtonByteBuffer(new byte[] { 0, 1, 2, 3, 4, 5 });
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(source);
+
+        assertEquals(0, source.getReadableBytes());
+        assertEquals(source.getWriteIndex(), buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < source.getReadableBytes(); ++i) {
+            assertEquals(source.getByte(i), buffer.readByte());
+        }
+
+        assertEquals(source.getReadableBytes(), buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesFromProtonBufferWithLength() {
+        ProtonBuffer source = new ProtonByteBuffer(new byte[] { 0, 1, 2, 3, 4, 5 });
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        try {
+            buffer.writeBytes(source, source.getReadableBytes() + 1);
+            fail("Should not write when length given is to large.");
+        } catch (IndexOutOfBoundsException ex) {}
+
+        buffer.writeBytes(source, source.getReadableBytes());
+
+        assertEquals(0, source.getReadableBytes());
+        assertEquals(source.getWriteIndex(), buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < source.getReadableBytes(); ++i) {
+            assertEquals(source.getByte(i), buffer.readByte());
+        }
+
+        assertEquals(source.getReadableBytes(), buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesFromProtonBufferWithLengthAndOffset() {
+        ProtonBuffer source = new ProtonByteBuffer(new byte[] { 0, 1, 2, 3, 4, 5 });
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(source, 0, source.getReadableBytes());
+
+        assertEquals(source.getReadableBytes(), buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < source.getReadableBytes(); ++i) {
+            assertEquals(source.getByte(i), buffer.readByte());
+        }
+
+        assertEquals(source.getReadableBytes(), buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteBytesFromByteBuffer() {
+        ByteBuffer source = ByteBuffer.wrap(new byte[] { 0, 1, 2, 3, 4, 5 });
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBytes(source);
+
+        assertEquals(0, source.remaining());
+        assertEquals(source.position(), buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < source.capacity(); ++i) {
+            assertEquals(source.get(i), buffer.readByte());
+        }
+    }
+
+    //----- Write Primitives Tests -------------------------------------------//
+
+    @Test
+    public void testWriteByte() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeByte((byte) 56);
+
+        assertEquals(1, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(56, buffer.readByte());
+
+        assertEquals(1, buffer.getWriteIndex());
+        assertEquals(1, buffer.getReadIndex());
+
+        assertEquals(0, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testWriteBoolean() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeBoolean(true);
+        buffer.writeBoolean(false);
+
+        assertEquals(2, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(true, buffer.readBoolean());
+
+        assertEquals(2, buffer.getWriteIndex());
+        assertEquals(1, buffer.getReadIndex());
+
+        assertEquals(1, buffer.getReadableBytes());
+        assertEquals(false, buffer.readBoolean());
+        assertEquals(2, buffer.getWriteIndex());
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testWriteShort() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeShort((short) 42);
+
+        assertEquals(2, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(42, buffer.readShort());
+
+        assertEquals(2, buffer.getWriteIndex());
+        assertEquals(2, buffer.getReadIndex());
+
+        assertEquals(0, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testWriteInt() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeInt(72);
+
+        assertEquals(4, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(72, buffer.readInt());
+
+        assertEquals(4, buffer.getWriteIndex());
+        assertEquals(4, buffer.getReadIndex());
+
+        assertEquals(0, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testWriteLong() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeLong(500l);
+
+        assertEquals(8, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(500l, buffer.readLong());
+
+        assertEquals(8, buffer.getWriteIndex());
+        assertEquals(8, buffer.getReadIndex());
+
+        assertEquals(0, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testWriteFloat() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeFloat(35.5f);
+
+        assertEquals(4, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(35.5f, buffer.readFloat(), 0.4f);
+
+        assertEquals(4, buffer.getWriteIndex());
+        assertEquals(4, buffer.getReadIndex());
+
+        assertEquals(0, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testWriteDouble() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        assertEquals(0, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.writeDouble(1.66);
+
+        assertEquals(8, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(1.66, buffer.readDouble(), 0.1);
+
+        assertEquals(8, buffer.getWriteIndex());
+        assertEquals(8, buffer.getReadIndex());
+
+        assertEquals(0, buffer.getReadableBytes());
+    }
+
+    //----- Tests for read operations ----------------------------------------//
+
+    @Test
+    public void testReadByte() {
+        byte[] source = new byte[] { 0, 1, 2, 3, 4, 5 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; ++i) {
+            assertEquals(source[i], buffer.readByte());
+        }
+
+        try {
+            buffer.readByte();
+            fail("Should not be able to read beyond current capacity");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testReadBoolean() {
+        byte[] source = new byte[] { 0, 1, 0, 1, 0, 1 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; ++i) {
+            if ((i % 2) == 0) {
+                assertFalse(buffer.readBoolean());
+            } else {
+                assertTrue(buffer.readBoolean());
+            }
+        }
+
+        try {
+            buffer.readBoolean();
+            fail("Should not be able to read beyond current capacity");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testReadShort() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.writeShort((short) 2);
+        buffer.writeShort((short) 20);
+        buffer.writeShort((short) 200);
+        buffer.writeShort((short) 256);
+        buffer.writeShort((short) 512);
+        buffer.writeShort((short) 1025);
+        buffer.writeShort((short) 32767);
+        buffer.writeShort((short) -1);
+        buffer.writeShort((short) -8757);
+
+        assertEquals(2, buffer.readShort());
+        assertEquals(20, buffer.readShort());
+        assertEquals(200, buffer.readShort());
+        assertEquals(256, buffer.readShort());
+        assertEquals(512, buffer.readShort());
+        assertEquals(1025, buffer.readShort());
+        assertEquals(32767, buffer.readShort());
+        assertEquals(-1, buffer.readShort());
+        assertEquals(-8757, buffer.readShort());
+
+        try {
+            buffer.readShort();
+            fail("Should not be able to read beyond current capacity");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testReadInt() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.writeInt((short) 2);
+        buffer.writeInt((short) 20);
+        buffer.writeInt((short) 200);
+        buffer.writeInt((short) 256);
+        buffer.writeInt((short) 512);
+        buffer.writeInt((short) 1025);
+        buffer.writeInt((short) 32767);
+        buffer.writeInt((short) -1);
+        buffer.writeInt((short) -8757);
+
+        assertEquals(2, buffer.readInt());
+        assertEquals(20, buffer.readInt());
+        assertEquals(200, buffer.readInt());
+        assertEquals(256, buffer.readInt());
+        assertEquals(512, buffer.readInt());
+        assertEquals(1025, buffer.readInt());
+        assertEquals(32767, buffer.readInt());
+        assertEquals(-1, buffer.readInt());
+        assertEquals(-8757, buffer.readInt());
+
+        try {
+            buffer.readInt();
+            fail("Should not be able to read beyond current capacity");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testReadLong() {
+        // This is not a capacity increase handling test so allocate with enough capacity for this test.
+        ProtonBuffer buffer = allocateBuffer(DEFAULT_CAPACITY * 2);
+
+        buffer.writeLong((short) 2);
+        buffer.writeLong((short) 20);
+        buffer.writeLong((short) 200);
+        buffer.writeLong((short) 256);
+        buffer.writeLong((short) 512);
+        buffer.writeLong((short) 1025);
+        buffer.writeLong((short) 32767);
+        buffer.writeLong((short) -1);
+        buffer.writeLong((short) -8757);
+
+        assertEquals(2, buffer.readLong());
+        assertEquals(20, buffer.readLong());
+        assertEquals(200, buffer.readLong());
+        assertEquals(256, buffer.readLong());
+        assertEquals(512, buffer.readLong());
+        assertEquals(1025, buffer.readLong());
+        assertEquals(32767, buffer.readLong());
+        assertEquals(-1, buffer.readLong());
+        assertEquals(-8757, buffer.readLong());
+
+        try {
+            buffer.readLong();
+            fail("Should not be able to read beyond current readable bytes");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testReadFloat() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.writeFloat(1.111f);
+        buffer.writeFloat(2.222f);
+        buffer.writeFloat(3.333f);
+
+        assertEquals(1.111f, buffer.readFloat(), 0.111f);
+        assertEquals(2.222f, buffer.readFloat(), 0.222f);
+        assertEquals(3.333f, buffer.readFloat(), 0.333f);
+
+        try {
+            buffer.readFloat();
+            fail("Should not be able to read beyond current capacity");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    @Test
+    public void testReadDouble() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.writeDouble(1.111);
+        buffer.writeDouble(2.222);
+        buffer.writeDouble(3.333);
+
+        assertEquals(1.111, buffer.readDouble(), 0.111);
+        assertEquals(2.222, buffer.readDouble(), 0.222);
+        assertEquals(3.333, buffer.readDouble(), 0.333);
+
+        try {
+            buffer.readDouble();
+            fail("Should not be able to read beyond current capacity");
+        } catch (IndexOutOfBoundsException ex) {}
+    }
+
+    //----- Tests for get operations -----------------------------------------//
+
+    @Test
+    public void testGetByte() {
+        byte[] source = new byte[] { 0, 1, 2, 3, 4, 5 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; ++i) {
+            assertEquals(source[i], buffer.getByte(i));
+        }
+
+        try {
+            buffer.readByte();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetUnsignedByte() {
+        byte[] source = new byte[] { 0, 1, 2, 3, 4, 5 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; ++i) {
+            assertEquals(source[i], buffer.getUnsignedByte(i));
+        }
+
+        try {
+            buffer.readByte();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetBoolean() {
+        byte[] source = new byte[] { 0, 1, 0, 1, 0, 1 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; ++i) {
+            if ((i % 2) == 0) {
+                assertFalse(buffer.getBoolean(i));
+            } else {
+                assertTrue(buffer.getBoolean(i));
+            }
+        }
+
+        try {
+            buffer.readBoolean();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetShort() {
+        byte[] source = new byte[] { 0, 0, 0, 1, 0, 2, 0, 3, 0, 4 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; i += 2) {
+            assertEquals(source[i + 1], buffer.getShort(i));
+        }
+
+        try {
+            buffer.readShort();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetUnsignedShort() {
+        byte[] source = new byte[] { 0, 0, 0, 1, 0, 2, 0, 3, 0, 4 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; i += 2) {
+            assertEquals(source[i + 1], buffer.getUnsignedShort(i));
+        }
+
+        try {
+            buffer.readShort();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetInt() {
+        byte[] source = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; i += 4) {
+            assertEquals(source[i + 3], buffer.getInt(i));
+        }
+
+        try {
+            buffer.readInt();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetUnsignedInt() {
+        byte[] source = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; i += 4) {
+            assertEquals(source[i + 3], buffer.getUnsignedInt(i));
+        }
+
+        try {
+            buffer.readInt();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetLong() {
+        byte[] source = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0,
+                                     0, 0, 0, 0, 0, 0, 0, 1,
+                                     0, 0, 0, 0, 0, 0, 0, 2,
+                                     0, 0, 0, 0, 0, 0, 0, 3,
+                                     0, 0, 0, 0, 0, 0, 0, 4 };
+        ProtonBuffer buffer = wrapBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+
+        for (int i = 0; i < source.length; i += 8) {
+            assertEquals(source[i + 7], buffer.getLong(i));
+        }
+
+        try {
+            buffer.readLong();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetFloat() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        buffer.writeInt(Float.floatToIntBits(1.1f));
+        buffer.writeInt(Float.floatToIntBits(2.2f));
+        buffer.writeInt(Float.floatToIntBits(42.3f));
+
+        assertEquals(Integer.BYTES * 3, buffer.getReadableBytes());
+
+        assertEquals(1.1f, buffer.getFloat(0), 0.1);
+        assertEquals(2.2f, buffer.getFloat(4), 0.1);
+        assertEquals(42.3f, buffer.getFloat(8), 0.1);
+
+        try {
+            buffer.readFloat();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    @Test
+    public void testGetDouble() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        buffer.writeLong(Double.doubleToLongBits(1.1));
+        buffer.writeLong(Double.doubleToLongBits(2.2));
+        buffer.writeLong(Double.doubleToLongBits(42.3));
+
+        assertEquals(Long.BYTES * 3, buffer.getReadableBytes());
+
+        assertEquals(1.1, buffer.getDouble(0), 0.1);
+        assertEquals(2.2, buffer.getDouble(8), 0.1);
+        assertEquals(42.3, buffer.getDouble(16), 0.1);
+
+        try {
+            buffer.readDouble();
+        } catch (IndexOutOfBoundsException ex) {
+            fail("Should be able to read from the buffer");
+        }
+    }
+
+    //----- Tests for Copy operations ----------------------------------------//
+
+    @Test
+    public void testCopyEmptyBuffer() {
+        ProtonBuffer buffer = allocateBuffer(10);
+        ProtonBuffer copy = buffer.copy();
+
+        assertEquals(buffer.getReadableBytes(), copy.getReadableBytes());
+
+        if (buffer.hasArray()) {
+            assertTrue(copy.hasArray());
+            assertNotNull(copy.getArray());
+            assertNotSame(buffer.getArray(), copy.getArray());
+        }
+    }
+
+    @Test
+    public void testCopyBuffer() {
+        ProtonBuffer buffer = allocateBuffer(10);
+
+        buffer.writeByte(1);
+        buffer.writeByte(2);
+        buffer.writeByte(3);
+        buffer.writeByte(4);
+        buffer.writeByte(5);
+
+        ProtonBuffer copy = buffer.copy();
+
+        assertEquals(buffer.getReadableBytes(), copy.getReadableBytes());
+
+        for (int i = 0; i < 5; ++i) {
+            assertEquals(buffer.getByte(i), copy.getByte(i));
+        }
+    }
+
+    @Test
+    public void testCopy() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity(); i ++) {
+            byte value = (byte) random.nextInt();
+            buffer.setByte(i, value);
+        }
+
+        final int readerIndex = LARGE_CAPACITY / 3;
+        final int writerIndex = LARGE_CAPACITY * 2 / 3;
+        buffer.setIndex(readerIndex, writerIndex);
+
+        // Make sure all properties are copied.
+        ProtonBuffer copy = buffer.copy();
+        assertEquals(0, copy.getReadIndex());
+        assertEquals(buffer.getReadableBytes(), copy.getWriteIndex());
+        assertEquals(buffer.getReadableBytes(), copy.capacity());
+        for (int i = 0; i < copy.capacity(); i ++) {
+            assertEquals(buffer.getByte(i + readerIndex), copy.getByte(i));
+        }
+
+        // Make sure the buffer content is independent from each other.
+        buffer.setByte(readerIndex, (byte) (buffer.getByte(readerIndex) + 1));
+        assertTrue(buffer.getByte(readerIndex) != copy.getByte(0));
+        copy.setByte(1, (byte) (copy.getByte(1) + 1));
+        assertTrue(buffer.getByte(readerIndex + 1) != copy.getByte(1));
+    }
+
+    @Test
+    public void testSequentialRandomFilledBufferIndexedCopy() {
+        doTestSequentialRandomFilledBufferIndexedCopy(false);
+    }
+
+    @Test
+    public void testSequentialRandomFilledBufferIndexedCopyDirectBackedBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialRandomFilledBufferIndexedCopy(true);
+    }
+
+    private void doTestSequentialRandomFilledBufferIndexedCopy(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            buffer.setBytes(i, value);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            ProtonBuffer copy = buffer.copy(i, BLOCK_SIZE);
+            for (int j = 0; j < BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), copy.getByte(j));
+            }
+        }
+    }
+
+    //----- Tests for Buffer duplication -------------------------------------//
+
+    @Test
+    public void testDuplicateEmptyBuffer() {
+        ProtonBuffer buffer = new ProtonByteBuffer(10);
+        ProtonBuffer duplicate = buffer.duplicate();
+
+        assertNotSame(buffer, duplicate);
+        assertEquals(buffer.capacity(), duplicate.capacity());
+        assertEquals(buffer.getReadableBytes(), duplicate.getReadableBytes());
+
+        if (buffer.hasArray()) {
+            assertSame(buffer.getArray(), duplicate.getArray());
+            assertEquals(0, buffer.getArrayOffset());
+        }
+    }
+
+    @Test
+    public void testDuplicate() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity(); i ++) {
+            byte value = (byte) random.nextInt();
+            buffer.setByte(i, value);
+        }
+
+        final int readerIndex = LARGE_CAPACITY / 3;
+        final int writerIndex = LARGE_CAPACITY * 2 / 3;
+        buffer.setIndex(readerIndex, writerIndex);
+
+        // Make sure all properties are copied.
+        ProtonBuffer duplicate = buffer.duplicate();
+        assertEquals(buffer.getReadableBytes(), duplicate.getReadableBytes());
+        assertEquals(0, buffer.compareTo(duplicate));
+
+        // Make sure the buffer content is shared.
+        buffer.setByte(readerIndex, (byte) (buffer.getByte(readerIndex) + 1));
+        assertEquals(buffer.getByte(readerIndex), duplicate.getByte(duplicate.getReadIndex()));
+        duplicate.setByte(duplicate.getReadIndex(), (byte) (duplicate.getByte(duplicate.getReadIndex()) + 1));
+        assertEquals(buffer.getByte(readerIndex), duplicate.getByte(duplicate.getReadIndex()));
+    }
+
+    //----- Tests for Buffer slicing -----------------------------------------//
+
+    @Test
+    public void testSliceIndex() throws Exception {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        assertEquals(0, buffer.slice(0, buffer.capacity()).getReadIndex());
+        assertEquals(0, buffer.slice(0, buffer.capacity() - 1).getReadIndex());
+        assertEquals(0, buffer.slice(1, buffer.capacity() - 1).getReadIndex());
+        assertEquals(0, buffer.slice(1, buffer.capacity() - 2).getReadIndex());
+
+        assertEquals(buffer.capacity(), buffer.slice(0, buffer.capacity()).getWriteIndex());
+        assertEquals(buffer.capacity() - 1, buffer.slice(0, buffer.capacity() - 1).getWriteIndex());
+        assertEquals(buffer.capacity() - 1, buffer.slice(1, buffer.capacity() - 1).getWriteIndex());
+        assertEquals(buffer.capacity() - 2, buffer.slice(1, buffer.capacity() - 2).getWriteIndex());
+    }
+
+    @Test
+    public void testAdvancingSlice() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            byte[] value = new byte[BLOCK_SIZE];
+            random.nextBytes(value);
+            assertEquals(0, buffer.getReadIndex());
+            assertEquals(i, buffer.getWriteIndex());
+            buffer.writeBytes(value);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValue = new byte[BLOCK_SIZE];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue);
+            assertEquals(i, buffer.getReadIndex());
+            assertEquals(LARGE_CAPACITY, buffer.getWriteIndex());
+            ProtonBuffer actualValue = buffer.slice().setWriteIndex(BLOCK_SIZE);
+            buffer.setReadIndex(BLOCK_SIZE + buffer.getReadIndex());
+            assertEquals(wrapBuffer(expectedValue), actualValue);
+
+            // Make sure if it is a sliced buffer.
+            actualValue.setByte(0, (byte) (actualValue.getByte(0) + 1));
+            assertEquals(buffer.getByte(i), actualValue.getByte(0));
+        }
+    }
+
+    @Test
+    public void testSequentialSlice1() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            byte[] value = new byte[BLOCK_SIZE];
+            random.nextBytes(value);
+            assertEquals(0, buffer.getReadIndex());
+            assertEquals(i, buffer.getWriteIndex());
+            buffer.writeBytes(value);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValue = new byte[BLOCK_SIZE];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue);
+            assertEquals(i, buffer.getReadIndex());
+            assertEquals(LARGE_CAPACITY, buffer.getWriteIndex());
+            ProtonBuffer actualValue = buffer.slice(buffer.getReadIndex(), BLOCK_SIZE);
+            buffer.setReadIndex(BLOCK_SIZE + buffer.getReadIndex());
+            assertEquals(new ProtonByteBuffer(expectedValue), actualValue);
+
+            // Make sure if it is a sliced buffer.
+            actualValue.setByte(0, (byte) (actualValue.getByte(0) + 1));
+            assertEquals(buffer.getByte(i), actualValue.getByte(0));
+        }
+    }
+
+    //----- Tests for conversion to ByteBuffer -------------------------------//
+
+    @Test
+    public void testToByteBufferWithDataPresent() {
+        ProtonBuffer buffer = allocateBuffer(10);
+
+        buffer.writeByte(1);
+        buffer.writeByte(2);
+        buffer.writeByte(3);
+        buffer.writeByte(4);
+        buffer.writeByte(5);
+
+        ByteBuffer byteBuffer = buffer.toByteBuffer();
+
+        assertEquals(buffer.getReadableBytes(), byteBuffer.limit());
+
+        if (buffer.hasArray()) {
+            assertTrue(byteBuffer.hasArray());
+            assertNotNull(byteBuffer.array());
+            assertSame(buffer.getArray(), byteBuffer.array());
+        }
+    }
+
+    @Test
+    public void testToByteBufferWhenNoData() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+        ByteBuffer byteBuffer = buffer.toByteBuffer();
+
+        assertEquals(buffer.getReadableBytes(), byteBuffer.limit());
+
+        if (buffer.hasArray()) {
+            assertTrue(byteBuffer.hasArray());
+            assertNotNull(byteBuffer.array());
+            assertSame(buffer.getArray(), byteBuffer.array());
+        }
+    }
+
+    @Test
+    public void testNioBufferNoArgVariant() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        byte[] value = new byte[buffer.capacity()];
+        random.nextBytes(value);
+        buffer.clear();
+        buffer.writeBytes(value);
+
+        assertRemainingEquals(ByteBuffer.wrap(value), buffer.toByteBuffer());
+    }
+
+    @Test
+    public void testToByteBufferWithRange() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        byte[] value = new byte[buffer.capacity()];
+        random.nextBytes(value);
+        buffer.clear();
+        buffer.writeBytes(value);
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            assertRemainingEquals(ByteBuffer.wrap(value, i, BLOCK_SIZE), buffer.toByteBuffer(i, BLOCK_SIZE));
+        }
+    }
+
+    //----- Tests for string conversion --------------------------------------//
+
+    @Test
+    public void testToStringFromUTF8() throws Exception {
+        String sourceString = "Test-String-1";
+
+        ProtonBuffer buffer = wrapBuffer(sourceString.getBytes(StandardCharsets.UTF_8));
+
+        String decoded = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(sourceString, decoded);
+    }
+
+    @Test
+    public void testReadUnicodeStringAcrossArrayBoundries() throws IOException {
+        String expected = "\u1f4a9\\u1f4a9\\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[utf8.length - 1];
+
+        System.arraycopy(utf8, 1, slice2, 0, slice2.length);
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(slice1);
+        buffer.append(slice2);
+
+        String result = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Test
+    public void testReadUnicodeStringAcrossMultipleArrayBoundries() throws IOException {
+        String expected = "\u1f4a9\\u1f4a9\\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[] { utf8[1], utf8[2] };
+        byte[] slice3 = new byte[] { utf8[3], utf8[4] };
+        byte[] slice4 = new byte[utf8.length - 5];
+
+        System.arraycopy(utf8, 5, slice4, 0, slice4.length);
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(slice1);
+        buffer.append(slice2);
+        buffer.append(slice3);
+        buffer.append(slice4);
+
+        String result = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    //----- Tests for index marking ------------------------------------------//
+
+    @Test
+    public void testMarkAndResetReadIndex() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.writeByte(0).writeByte(1);
+        buffer.markReadIndex();
+
+        assertEquals(0, buffer.readByte());
+        assertEquals(1, buffer.readByte());
+
+        buffer.resetReadIndex();
+
+        assertEquals(0, buffer.readByte());
+        assertEquals(1, buffer.readByte());
+    }
+
+    @Test
+    public void testResetReadIndexWhenInvalid() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.writeByte(0).writeByte(1);
+        buffer.readByte();
+        buffer.readByte();
+        buffer.markReadIndex();
+        buffer.setIndex(0, 1);
+
+        try {
+            buffer.resetReadIndex();
+            fail("Should not be able to reset to invalid mark");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    @Test
+    public void testMarkAndResetWriteIndex() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.markWriteIndex();
+        buffer.writeByte(0).writeByte(1);
+        buffer.resetWriteIndex();
+        buffer.writeByte(2).writeByte(3);
+
+        assertEquals(2, buffer.readByte());
+        assertEquals(3, buffer.readByte());
+    }
+
+    @Test
+    public void testResetWriteIndexWhenInvalid() {
+        ProtonBuffer buffer = allocateDefaultBuffer();
+
+        buffer.markWriteIndex();
+        buffer.writeByte(0).writeByte(1);
+        buffer.readByte();
+        buffer.readByte();
+
+        try {
+            buffer.resetWriteIndex();
+            fail("Should not be able to reset to invalid mark");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    @Test
+    public void testReaderIndexLargerThanWriterIndex() {
+        String content1 = "hello";
+        String content2 = "world";
+        int length = content1.length() + content2.length();
+
+        ProtonBuffer buffer = allocateBuffer(length);
+        buffer.setIndex(0, 0);
+        buffer.writeBytes(content1.getBytes(StandardCharsets.UTF_8));
+        buffer.markWriteIndex();
+        buffer.skipBytes(content1.length());
+        buffer.writeBytes(content2.getBytes(StandardCharsets.UTF_8));
+        buffer.skipBytes(content2.length());
+        assertTrue(buffer.getReadIndex() <= buffer.getWriteIndex());
+
+        assertThrows(IndexOutOfBoundsException.class, () -> buffer.resetWriteIndex());
+    }
+
+    //----- Tests for equality and comparison --------------------------------//
+
+    @Test
+    public void testEqualsSelf() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer = wrapBuffer(payload);
+        assertTrue(buffer.equals(buffer));
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testEqualsFailsForOtherBufferType() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer = wrapBuffer(payload);
+        ByteBuffer byteBuffer = ByteBuffer.wrap(payload);
+
+        assertFalse(buffer.equals(byteBuffer));
+    }
+
+    @Test
+    public void testEqualsWithSameContents() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer1 = wrapBuffer(payload);
+        ProtonBuffer buffer2 = wrapBuffer(payload);
+
+        assertTrue(buffer1.equals(buffer2));
+        assertTrue(buffer2.equals(buffer1));
+    }
+
+    @Test
+    public void testEqualsWithSameContentDifferenceArrays() {
+        byte[] payload1 = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] payload2 = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer1 = wrapBuffer(payload1);
+        ProtonBuffer buffer2 = wrapBuffer(payload2);
+
+        assertTrue(buffer1.equals(buffer2));
+        assertTrue(buffer2.equals(buffer1));
+    }
+
+    @Test
+    public void testEqualsWithDiffereingContent() {
+        byte[] payload1 = new byte[] { 1, 2, 3, 4, 5 };
+        byte[] payload2 = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer1 = wrapBuffer(payload1);
+        ProtonBuffer buffer2 = wrapBuffer(payload2);
+
+        assertFalse(buffer1.equals(buffer2));
+        assertFalse(buffer2.equals(buffer1));
+    }
+
+    @Test
+    public void testEqualsWithDifferingReadableBytes() {
+        byte[] payload1 = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] payload2 = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer1 = wrapBuffer(payload1);
+        ProtonBuffer buffer2 = wrapBuffer(payload2);
+
+        buffer1.readByte();
+
+        assertFalse(buffer1.equals(buffer2));
+        assertFalse(buffer2.equals(buffer1));
+    }
+
+    @Test
+    public void testHashCode() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer = wrapBuffer(payload);
+        assertNotEquals(0, buffer.hashCode());
+        assertNotEquals(buffer.hashCode(), System.identityHashCode(buffer));
+    }
+
+    @Test
+    public void testCompareToSameContents() {
+        byte[] payload1 = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] payload2 = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer1 = wrapBuffer(payload1);
+        ProtonBuffer buffer2 = wrapBuffer(payload2);
+
+        assertEquals(0, buffer1.compareTo(buffer1));
+        assertEquals(0, buffer1.compareTo(buffer2));
+        assertEquals(0, buffer2.compareTo(buffer1));
+    }
+
+    @Test
+    public void testCompareToDifferentContents() {
+        byte[] payload1 = new byte[] { 1, 2, 3, 4, 5 };
+        byte[] payload2 = new byte[] { 0, 1, 2, 3, 4 };
+        ProtonBuffer buffer1 = wrapBuffer(payload1);
+        ProtonBuffer buffer2 = wrapBuffer(payload2);
+
+        assertEquals(1, buffer1.compareTo(buffer2));
+        assertEquals(-1, buffer2.compareTo(buffer1));
+    }
+
+    @Test
+    public void testComparableInterfaceNotViolatedWithLongWrites() {
+        ProtonBuffer buffer1 = allocateBuffer(LARGE_CAPACITY);
+        ProtonBuffer buffer2 = allocateBuffer(LARGE_CAPACITY);
+
+        buffer1.setWriteIndex(buffer1.getReadIndex());
+        buffer1.writeLong(0);
+
+        buffer2.setWriteIndex(buffer2.getReadIndex());
+        buffer2.writeLong(0xF0000000L);
+
+        assertTrue(buffer1.compareTo(buffer2) < 0);
+        assertTrue(buffer2.compareTo(buffer1) > 0);
+    }
+
+    @Test
+    public void testCompareToContract() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        try {
+            buffer.compareTo(null);
+            fail();
+        } catch (NullPointerException e) {
+            // Expected
+        }
+
+        // Fill the random stuff
+        byte[] value = new byte[32];
+        random.nextBytes(value);
+        // Prevent overflow / underflow
+        if (value[0] == 0) {
+            value[0]++;
+        } else if (value[0] == -1) {
+            value[0]--;
+        }
+
+        buffer.setIndex(0, value.length);
+        buffer.setBytes(0, value);
+
+        assertEquals(0, buffer.compareTo(new ProtonByteBuffer(value)));
+
+        value[0]++;
+        assertTrue(buffer.compareTo(new ProtonByteBuffer(value)) < 0);
+        value[0] -= 2;
+        assertTrue(buffer.compareTo(new ProtonByteBuffer(value)) > 0);
+        value[0]++;
+
+        assertTrue(buffer.compareTo(new ProtonByteBuffer(value, 0, 31)) > 0);
+        assertTrue(buffer.slice(0, 31).compareTo(new ProtonByteBuffer(value)) < 0);
+    }
+
+    //----- Tests for readBytes variants -------------------------------------//
+
+    @Test
+    public void testReadBytesSingleArray() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] target = new byte[payload.length];
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+
+        buffer.readBytes(target);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    @Test
+    public void testReadBytesArrayAndLength() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] target = new byte[payload.length];
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+
+        try {
+            buffer.readBytes(target, -1);
+            fail("should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        buffer.readBytes(target, target.length);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    @Test
+    public void testReadBytesArrayOffsetAndLength() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] target = new byte[payload.length];
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+
+        try {
+            buffer.readBytes(target, 0, -1);
+            fail("should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        buffer.readBytes(target, 0, target.length);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    @Test
+    public void testReadBytesSingleProtonBuffer() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+        ProtonBuffer target = allocateBuffer(5, 5);
+
+        buffer.readBytes(target);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        target.setWriteIndex(0);
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    @Test
+    public void testReadBytesProtonBufferAndLength() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+        ProtonBuffer target = allocateBuffer(5, 5);
+
+        try {
+            buffer.readBytes(target, -1);
+            fail("should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        try {
+            buffer.readBytes(target, 1024);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iae) {
+        }
+
+        buffer.readBytes(target, payload.length);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        target.setWriteIndex(0);
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    @Test
+    public void testReadBytesProtonBufferOffsetAndLength() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+        ProtonBuffer target = allocateBuffer(5, 5);
+
+        try {
+            buffer.readBytes(target, 0, -1);
+            fail("should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        try {
+            buffer.readBytes(target, -1, -1);
+            fail("should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        buffer.readBytes(target, 0, payload.length);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        target.setWriteIndex(0);
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    @Test
+    public void testReadBytesIntoByteBuffer() {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4 };
+
+        ProtonBuffer buffer = wrapBuffer(payload);
+        ByteBuffer target = ByteBuffer.allocate(5);
+
+        buffer.readBytes(target);
+
+        assertEquals(0, buffer.getReadableBytes());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        target.clear();
+
+        try {
+            buffer.readBytes(target);
+            fail("should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {
+        }
+    }
+
+    //----- Test for set by index --------------------------------------------//
+
+    @Test
+    public void testSetByteAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(5, 5);
+
+        for (int i = 0; i < buffer.capacity(); ++i) {
+            buffer.setByte(i, i);
+        }
+
+        for (int i = 0; i < buffer.capacity(); ++i) {
+            assertEquals(i, buffer.getByte(i));
+        }
+
+        try {
+            buffer.setByte(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setByte(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetBooleanAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(5, 5);
+
+        for (int i = 0; i < buffer.capacity(); ++i) {
+            if ((i % 2) == 0) {
+                buffer.setBoolean(i, false);
+            } else {
+                buffer.setBoolean(i, true);
+            }
+        }
+
+        for (int i = 0; i < buffer.capacity(); ++i) {
+            if ((i % 2) == 0) {
+                assertFalse(buffer.getBoolean(i));
+            } else {
+                assertTrue(buffer.getBoolean(i));
+            }
+        }
+
+        try {
+            buffer.setBoolean(-1, true);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setBoolean(buffer.capacity(), false);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetShortAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(10, 10);
+
+        for (short i = 0; i < buffer.capacity() / 2; i += 2) {
+            buffer.setShort(i, i);
+        }
+
+        for (short i = 0; i < buffer.capacity() / 2; i += 2) {
+            assertEquals(i, buffer.getShort(i));
+        }
+
+        try {
+            buffer.setShort(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setShort(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetIntAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(20, 20);
+
+        for (int i = 0; i < buffer.capacity() / 4; i += 4) {
+            buffer.setInt(i, i);
+        }
+
+        for (int i = 0; i < buffer.capacity() / 4; i += 4) {
+            assertEquals(i, buffer.getInt(i));
+        }
+
+        try {
+            buffer.setInt(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setInt(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetLongAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(40, 40);
+
+        for (long i = 0; i < buffer.capacity() / 8; i += 8) {
+            buffer.setLong((int) i, i);
+        }
+
+        for (long i = 0; i < buffer.capacity() / 8; i += 8) {
+            assertEquals(i, buffer.getLong((int) i));
+        }
+
+        try {
+            buffer.setInt(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setInt(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetFloatAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+
+        buffer.setFloat(0, 1.5f);
+        buffer.setFloat(4, 45.2f);
+
+        assertEquals(1.5f, buffer.getFloat(0), 0.1);
+        assertEquals(45.2f, buffer.getFloat(4), 0.1);
+
+        try {
+            buffer.setFloat(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setFloat(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetDoubleAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(16, 16);
+
+        buffer.setDouble(0, 1.5);
+        buffer.setDouble(8, 45.2);
+
+        assertEquals(1.5, buffer.getDouble(0), 0.1);
+        assertEquals(45.2, buffer.getDouble(8), 0.1);
+
+        try {
+            buffer.setDouble(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setDouble(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    @Test
+    public void testSetCharAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+
+        buffer.setChar(0, 65);
+        buffer.setChar(4, 66);
+
+        assertEquals('A', buffer.getChar(0));
+        assertEquals('B', buffer.getChar(4));
+
+        try {
+            buffer.setDouble(-1, 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+
+        try {
+            buffer.setDouble(buffer.capacity(), 0);
+            fail("should throw an IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException ioe) {}
+    }
+
+    //----- Test for setBytes methods ----------------------------------------//
+
+    @Test
+    public void testSetBytesUsingArray() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+
+        byte[] other = new byte[] { 1, 2 };
+
+        buffer.setWriteIndex(buffer.capacity());
+        buffer.setBytes(0, other);
+
+        assertEquals(1, buffer.readByte());
+        assertEquals(2, buffer.readByte());
+    }
+
+    @Test
+    public void testSetBytesUsingBufferAtIndex() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+        ProtonBuffer other = allocateBuffer(8, 8);
+
+        buffer.setWriteIndex(buffer.capacity());
+
+        other.writeByte(1);
+        other.writeByte(2);
+
+        buffer.setBytes(0, other);
+
+        assertEquals(1, buffer.readByte());
+        assertEquals(2, buffer.readByte());
+
+        assertTrue(other.getReadableBytes() == 0);
+        assertFalse(other.isReadable());
+    }
+
+    @Test
+    public void testSetBytesUsingBufferAtIndexWithLength() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+        ProtonBuffer other = allocateBuffer(8, 8);
+
+        buffer.setWriteIndex(buffer.capacity());
+
+        other.writeByte(1);
+        other.writeByte(2);
+
+        buffer.setBytes(0, other, 2);
+
+        assertEquals(1, buffer.readByte());
+        assertEquals(2, buffer.readByte());
+
+        assertTrue(other.getReadableBytes() == 0);
+        assertFalse(other.isReadable());
+    }
+
+    @Test
+    public void testSetBytesUsingBufferAtIndexHandleBoundsError() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+        ProtonBuffer other = allocateBuffer(8, 8);
+
+        buffer.setWriteIndex(buffer.capacity());
+
+        other.writeByte(0);
+        other.writeByte(1);
+
+        try {
+            buffer.setBytes(0, null, 0);
+            fail("Should thrown NullPointerException");
+        } catch (NullPointerException npe) {}
+
+        try {
+            buffer.setBytes(-1, other, 1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.setBytes(0, other, -1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.setBytes(-1, other, -1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.setBytes(10, other, 1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.setBytes(0, other, 10);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.setBytes(0, other, 3);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.setBytes(buffer.getWriteIndex() - 1, other, 2);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    @Test
+    public void testSetBytesFromLargerBufferIntoSmallerBuffer() {
+        ProtonBuffer buffer = allocateBuffer(2, 2);
+        ProtonBuffer other = allocateBuffer(3, 3);
+
+        other.writeByte(1);
+        other.writeByte(2);
+        other.writeByte(3);
+
+        try {
+            buffer.setBytes(0, other);
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        buffer.setBytes(0, other, 2);
+
+        assertFalse(buffer.isReadable());
+
+        buffer.setWriteIndex(2);
+
+        assertEquals(buffer.readByte(), 1);
+        assertEquals(buffer.readByte(), 2);
+
+        assertEquals(2, other.getReadIndex());
+    }
+
+    //----- Test for getBytes methods ----------------------------------------//
+
+    @Test
+    public void testGetBytesUsingBufferAtIndexHandleBoundsError() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+        ProtonBuffer other = allocateBuffer(8, 8);
+
+        buffer.setWriteIndex(buffer.capacity());
+
+        other.writeByte(0);
+        other.writeByte(1);
+
+        try {
+            buffer.getBytes(0, null, 0);
+            fail("Should thrown NullPointerException");
+        } catch (NullPointerException npe) {}
+
+        try {
+            buffer.getBytes(-1, other, 1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.getBytes(0, other, -1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.getBytes(-1, other, -1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.getBytes(10, other, 1);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.getBytes(0, other, 10);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.getBytes(buffer.getWriteIndex() - 1, other, 2);
+            fail("Should thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    @Test
+    public void testGetBytesUsingArray() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+
+        byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
+        byte[] target = new byte[data.length];
+
+        buffer.writeBytes(data);
+        buffer.getBytes(0, target);
+
+        for (int i = 0; i < data.length; ++i) {
+            assertEquals(buffer.getByte(i), target[i]);
+        }
+    }
+
+    @Test
+    public void testGetBytesUsingArrayOffsetAndLength() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+
+        byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
+        byte[] target = new byte[data.length];
+
+        buffer.writeBytes(data);
+        buffer.getBytes(0, target, 0, target.length);
+
+        for (int i = 0; i < data.length; ++i) {
+            assertEquals(buffer.getByte(i), target[i]);
+        }
+
+        byte[] target2 = new byte[data.length * 2];
+        buffer.getBytes(0, target2, target.length, target.length);
+
+        for (int i = 0; i < data.length; ++i) {
+            assertEquals(buffer.getByte(i), target2[i + target.length]);
+        }
+    }
+
+    @Test
+    public void testGetBytesUsingBuffer() {
+        ProtonBuffer buffer = allocateBuffer(8, 8);
+        ProtonBuffer target = allocateBuffer(8, 8);
+
+        byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
+
+        buffer.writeBytes(data);
+        buffer.getBytes(0, target);
+
+        assertTrue(target.isReadable());
+        assertEquals(8, target.getReadableBytes());
+
+        for (int i = 0; i < data.length; ++i) {
+            assertEquals(buffer.getByte(i), target.getByte(i));
+        }
+    }
+
+    @Test
+    public void testGetBytesFromLargerBufferIntoSmallerBuffer() {
+        ProtonBuffer buffer = allocateBuffer(2, 2);
+        ProtonBuffer other = allocateBuffer(3, 3);
+
+        other.writeByte(1);
+        other.writeByte(2);
+        other.writeByte(3);
+
+        other.getBytes(0, buffer);
+
+        assertTrue(buffer.isReadable());
+
+        assertEquals(other.readByte(), 1);
+        assertEquals(other.readByte(), 2);
+        assertEquals(other.readByte(), 3);
+
+        assertEquals(3, other.getReadIndex());
+
+        assertEquals(buffer.readByte(), 1);
+        assertEquals(buffer.readByte(), 2);
+    }
+
+    //----- Miscellaneous Stress Tests
+
+    @Test
+    public void testRandomByteAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity(); i ++) {
+            byte value = (byte) random.nextInt();
+            buffer.setByte(i, value);
+        }
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity(); i ++) {
+            byte value = (byte) random.nextInt();
+            assertEquals(value, buffer.getByte(i));
+        }
+    }
+
+    @Test
+    public void testRandomShortAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity() - 1; i += 2) {
+            short value = (short) random.nextInt();
+            buffer.setShort(i, value);
+        }
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity() - 1; i += 2) {
+            short value = (short) random.nextInt();
+            assertEquals(value, buffer.getShort(i));
+        }
+    }
+
+    @Test
+    public void testShortConsistentWithByteBuffer() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < 64; ++i) {
+            ByteBuffer javaBuffer = ByteBuffer.allocate(buffer.capacity());
+
+            short expected = (short) (random.nextInt() & 0xFFFF);
+            javaBuffer.putShort(expected);
+
+            final int bufferIndex = buffer.capacity() - 2;
+            buffer.setShort(bufferIndex, expected);
+            javaBuffer.flip();
+
+            short javaActual = javaBuffer.getShort();
+            assertEquals(expected, javaActual);
+            assertEquals(javaActual, buffer.getShort(bufferIndex));
+        }
+    }
+
+    @Test
+    public void testRandomIntAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity() - 3; i += 4) {
+            int value = random.nextInt();
+            buffer.setInt(i, value);
+        }
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity() - 3; i += 4) {
+            int value = random.nextInt();
+            assertEquals(value, buffer.getInt(i));
+        }
+    }
+
+    @Test
+    public void testIntConsistentWithByteBuffer() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < 64; ++i) {
+            ByteBuffer javaBuffer = ByteBuffer.allocate(buffer.capacity());
+            int expected = random.nextInt();
+            javaBuffer.putInt(expected);
+
+            final int bufferIndex = buffer.capacity() - 4;
+            buffer.setInt(bufferIndex, expected);
+            javaBuffer.flip();
+
+            int javaActual = javaBuffer.getInt();
+            assertEquals(expected, javaActual);
+            assertEquals(javaActual, buffer.getInt(bufferIndex));
+        }
+    }
+
+    @Test
+    public void testRandomLongAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity() - 7; i += 8) {
+            long value = random.nextLong();
+            buffer.setLong(i, value);
+        }
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity() - 7; i += 8) {
+            long value = random.nextLong();
+            assertEquals(value, buffer.getLong(i));
+        }
+    }
+
+    @Test
+    public void testLongConsistentWithByteBuffer() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < 64; ++i) {
+            ByteBuffer javaBuffer = ByteBuffer.allocate(buffer.capacity());
+
+            long expected = random.nextLong();
+            javaBuffer.putLong(expected);
+
+            final int bufferIndex = buffer.capacity() - 8;
+            buffer.setLong(bufferIndex, expected);
+            javaBuffer.flip();
+
+            long javaActual = javaBuffer.getLong();
+            assertEquals(expected, javaActual);
+            assertEquals(javaActual, buffer.getLong(bufferIndex));
+        }
+    }
+
+    @Test
+    public void testRandomFloatAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity() - 7; i += 8) {
+            float value = random.nextFloat();
+            buffer.setFloat(i, value);
+        }
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity() - 7; i += 8) {
+            float expected = random.nextFloat();
+            float actual = buffer.getFloat(i);
+            assertEquals(expected, actual, 0.01);
+        }
+    }
+
+    @Test
+    public void testRandomDoubleAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        for (int i = 0; i < buffer.capacity() - 7; i += 8) {
+            double value = random.nextDouble();
+            buffer.setDouble(i, value);
+        }
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity() - 7; i += 8) {
+            double expected = random.nextDouble();
+            double actual = buffer.getDouble(i);
+            assertEquals(expected, actual, 0.01);
+        }
+    }
+
+    @Test
+    public void testSequentialByteAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity(); i ++) {
+            byte value = (byte) random.nextInt();
+            assertEquals(i, buffer.getWriteIndex());
+            assertTrue(buffer.isWritable());
+            buffer.writeByte(value);
+        }
+
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isWritable());
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity(); i ++) {
+            byte value = (byte) random.nextInt();
+            assertEquals(i, buffer.getReadIndex());
+            assertTrue(buffer.isReadable());
+            assertEquals(value, buffer.readByte());
+        }
+
+        assertEquals(buffer.capacity(), buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isReadable());
+        assertFalse(buffer.isWritable());
+    }
+
+    @Test
+    public void testSequentialShortAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity(); i += 2) {
+            short value = (short) random.nextInt();
+            assertEquals(i, buffer.getWriteIndex());
+            assertTrue(buffer.isWritable());
+            buffer.writeShort(value);
+        }
+
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isWritable());
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity(); i += 2) {
+            short value = (short) random.nextInt();
+            assertEquals(i, buffer.getReadIndex());
+            assertTrue(buffer.isReadable());
+            assertEquals(value, buffer.readShort());
+        }
+
+        assertEquals(buffer.capacity(), buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isReadable());
+        assertFalse(buffer.isWritable());
+    }
+
+    @Test
+    public void testSequentialIntAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity(); i += 4) {
+            int value = random.nextInt();
+            assertEquals(i, buffer.getWriteIndex());
+            assertTrue(buffer.isWritable());
+            buffer.writeInt(value);
+        }
+
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isWritable());
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity(); i += 4) {
+            int value = random.nextInt();
+            assertEquals(i, buffer.getReadIndex());
+            assertTrue(buffer.isReadable());
+            assertEquals(value, buffer.readInt());
+        }
+
+        assertEquals(buffer.capacity(), buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isReadable());
+        assertFalse(buffer.isWritable());
+    }
+
+    @Test
+    public void testSequentialLongAccess() {
+        ProtonBuffer buffer = allocateBuffer(LARGE_CAPACITY);
+
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity(); i += 8) {
+            long value = random.nextLong();
+            assertEquals(i, buffer.getWriteIndex());
+            assertTrue(buffer.isWritable());
+            buffer.writeLong(value);
+        }
+
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isWritable());
+
+        random.setSeed(seed);
+        for (int i = 0; i < buffer.capacity(); i += 8) {
+            long value = random.nextLong();
+            assertEquals(i, buffer.getReadIndex());
+            assertTrue(buffer.isReadable());
+            assertEquals(value, buffer.readLong());
+        }
+
+        assertEquals(buffer.capacity(), buffer.getReadIndex());
+        assertEquals(buffer.capacity(), buffer.getWriteIndex());
+        assertFalse(buffer.isReadable());
+        assertFalse(buffer.isWritable());
+    }
+
+    @Test
+    public void testByteArrayTransfer() {
+        testByteArrayTransfer(false);
+    }
+
+    @Test
+    public void testByteArrayTransferDirectBackedBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        testByteArrayTransfer(true);
+    }
+
+    private void testByteArrayTransfer(boolean direct) {
+        final ProtonBuffer buffer;
+
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE * 2];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValue = new byte[BLOCK_SIZE * 2];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue[j], value[j]);
+            }
+        }
+    }
+
+    @Test
+    public void testRandomByteArrayTransfer1() {
+        doTestRandomByteArrayTransfer1(false);
+    }
+
+    @Test
+    public void testRandomByteArrayTransfer1DirectBackedBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomByteArrayTransfer1(true);
+    }
+
+    private void doTestRandomByteArrayTransfer1(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            buffer.setBytes(i, value);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            buffer.getBytes(i, value);
+            for (int j = 0; j < BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value[j]);
+            }
+        }
+    }
+
+    @Test
+    public void testRandomByteArrayTransfer2() {
+        dotestRandomByteArrayTransfer2(false);
+    }
+
+    @Test
+    public void testRandomByteArrayTransfer2DirectBackedBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        dotestRandomByteArrayTransfer2(true);
+    }
+
+    private void dotestRandomByteArrayTransfer2(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE * 2];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE * 2];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value[j]);
+            }
+        }
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer1() {
+        doTestRandomProtonBufferTransfer1(false, false);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer1DirectSource() {
+        doTestRandomProtonBufferTransfer1(true, false);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer1DirectTarget() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomProtonBufferTransfer1(false, true);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer1DirectSourceAndTarget() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomProtonBufferTransfer1(true, true);
+    }
+
+    private void doTestRandomProtonBufferTransfer1(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] valueContent = new byte[BLOCK_SIZE];
+        final ProtonBuffer value;
+        if (directSource) {
+            value = new ProtonNioByteBuffer(ByteBuffer.allocateDirect(BLOCK_SIZE));
+        } else {
+            value = new ProtonByteBuffer(BLOCK_SIZE, BLOCK_SIZE);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear();
+            value.writeBytes(valueContent);
+            buffer.setBytes(i, value);
+            assertEquals(BLOCK_SIZE, value.getReadIndex());
+            assertEquals(BLOCK_SIZE, value.getWriteIndex());
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            value.clear();
+            buffer.getBytes(i, value);
+            assertEquals(0, value.getReadIndex());
+            assertEquals(BLOCK_SIZE, value.getWriteIndex());
+            for (int j = 0; j < BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+        }
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer2() {
+        doTestRandomProtonBufferTransfer2(false, false);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer2DirectSource() {
+        doTestRandomProtonBufferTransfer2(true, false);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer2DirectTarget() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomProtonBufferTransfer2(false, true);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer2DirectSourceAndTarget() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomProtonBufferTransfer2(true, true);
+    }
+
+    private void doTestRandomProtonBufferTransfer2(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        final int SIZE = BLOCK_SIZE * 2;
+        byte[] valueContent = new byte[SIZE];
+        final ProtonBuffer value;
+        if (directSource) {
+            value = new ProtonNioByteBuffer(ByteBuffer.allocateDirect(SIZE));
+        } else {
+            value = new ProtonByteBuffer(SIZE, SIZE);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear();
+            value.writeBytes(valueContent);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[SIZE];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+        }
+    }
+
+    @Test
+    public void testRandomByteBufferTransfer() {
+        doTestRandomByteBufferTransfer(false, false);
+    }
+
+    @Test
+    public void testRandomDirectByteBufferTransfer() {
+        doTestRandomByteBufferTransfer(true, false);
+    }
+
+    @Test
+    public void testRandomByteBufferTransferToDirectProtonBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomByteBufferTransfer(false, true);
+    }
+
+    @Test
+    public void testRandomDirectByteBufferTransferToDirectProtonBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomByteBufferTransfer(true, true);
+    }
+
+    private void doTestRandomByteBufferTransfer(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        final int SIZE = BLOCK_SIZE * 2;
+        final byte[] valueContent = new byte[SIZE];
+        final ByteBuffer value;
+        if (directSource) {
+            value = ByteBuffer.allocateDirect(BLOCK_SIZE * 2);
+        } else {
+            value = ByteBuffer.allocate(BLOCK_SIZE * 2);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear();
+            value.put(valueContent);
+            value.clear().position(random.nextInt(BLOCK_SIZE));
+            value.limit(value.position() + BLOCK_SIZE);
+            buffer.setBytes(i, value);
+        }
+
+        random.setSeed(seed);
+        ByteBuffer expectedValue = ByteBuffer.allocate(BLOCK_SIZE * 2);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue.array());
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            value.clear().position(valueOffset).limit(valueOffset + BLOCK_SIZE);
+            buffer.getBytes(i, value);
+            assertEquals(valueOffset + BLOCK_SIZE, value.position());
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.get(j), value.get(j));
+            }
+        }
+    }
+
+    @Test
+    public void testSequentialByteArrayTransfer1() {
+        dotestSequentialByteArrayTransfer1(false);
+    }
+
+    @Test
+    public void testSequentialByteArrayTransfer1DirectBackedBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        dotestSequentialByteArrayTransfer1(true);
+    }
+
+    private void dotestSequentialByteArrayTransfer1(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE];
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            assertEquals(0, buffer.getReadIndex());
+            assertEquals(i, buffer.getWriteIndex());
+            buffer.writeBytes(value);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValue = new byte[BLOCK_SIZE];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue);
+            assertEquals(i, buffer.getReadIndex());
+            assertEquals(LARGE_CAPACITY, buffer.getWriteIndex());
+            buffer.readBytes(value);
+            for (int j = 0; j < BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue[j], value[j]);
+            }
+        }
+    }
+
+    @Test
+    public void testSequentialByteArrayTransfer2() {
+        doTestSequentialByteArrayTransfer2(false);
+    }
+
+    @Test
+    public void testSequentialByteArrayTransfer2DirectBackedBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialByteArrayTransfer2(true);
+    }
+
+    private void doTestSequentialByteArrayTransfer2(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE * 2];
+        buffer.setWriteIndex(0);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            assertEquals(0, buffer.getReadIndex());
+            assertEquals(i, buffer.getWriteIndex());
+            int readerIndex = random.nextInt(BLOCK_SIZE);
+            buffer.writeBytes(value, readerIndex, BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValue = new byte[BLOCK_SIZE * 2];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            assertEquals(i, buffer.getReadIndex());
+            assertEquals(LARGE_CAPACITY, buffer.getWriteIndex());
+            buffer.readBytes(value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue[j], value[j]);
+            }
+        }
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer1() {
+        doTestSequentialProtonBufferTransfer1(false, false);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer1DirectSource() {
+        doTestSequentialProtonBufferTransfer1(true, false);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer1DirectTargetBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialProtonBufferTransfer1(false, true);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer1DirectSourceAndTargetBuffers() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialProtonBufferTransfer1(true, true);
+    }
+
+    private void doTestSequentialProtonBufferTransfer1(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        final int SIZE = BLOCK_SIZE * 2;
+        final byte[] valueContent = new byte[SIZE];
+        final ProtonBuffer value;
+        if (directSource) {
+            value = new ProtonNioByteBuffer(ByteBuffer.allocateDirect(SIZE));
+        } else {
+            value = new ProtonByteBuffer(SIZE, SIZE);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear().writeBytes(valueContent);
+            assertEquals(0, buffer.getReadIndex());
+            assertEquals(i, buffer.getWriteIndex());
+            buffer.writeBytes(value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+            assertEquals(0, value.getReadIndex());
+            assertEquals(valueContent.length, value.getWriteIndex());
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE * 2];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            assertEquals(i, buffer.getReadIndex());
+            assertEquals(LARGE_CAPACITY, buffer.getWriteIndex());
+            buffer.readBytes(value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+            assertEquals(0, value.getReadIndex());
+            assertEquals(valueContent.length, value.getWriteIndex());
+        }
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer2() {
+        doTestSequentialProtonBufferTransfer2(false, false);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer2DirectSourceBuffer() {
+        doTestSequentialProtonBufferTransfer2(true, false);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer2DirectTargetBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialProtonBufferTransfer2(false, true);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer2DirectSourceAndTargetBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialProtonBufferTransfer2(true, true);
+    }
+
+    private void doTestSequentialProtonBufferTransfer2(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        final int SIZE = BLOCK_SIZE * 2;
+        final byte[] valueContent = new byte[SIZE];
+        final ProtonBuffer value;
+        if (directSource) {
+            value = new ProtonNioByteBuffer(ByteBuffer.allocateDirect(SIZE));
+        } else {
+            value = new ProtonByteBuffer(SIZE, SIZE);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear().writeBytes(valueContent);
+            assertEquals(0, buffer.getReadIndex());
+            assertEquals(i, buffer.getWriteIndex());
+            int readerIndex = random.nextInt(BLOCK_SIZE);
+            value.setReadIndex(readerIndex);
+            value.setWriteIndex(readerIndex + BLOCK_SIZE);
+            buffer.writeBytes(value);
+            assertEquals(readerIndex + BLOCK_SIZE, value.getWriteIndex());
+            assertEquals(value.getWriteIndex(), value.getReadIndex());
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE * 2];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            assertEquals(i, buffer.getReadIndex());
+            assertEquals(LARGE_CAPACITY, buffer.getWriteIndex());
+            value.setReadIndex(valueOffset);
+            value.setWriteIndex(valueOffset);
+            buffer.readBytes(value, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+            assertEquals(valueOffset, value.getReadIndex());
+            assertEquals(valueOffset + BLOCK_SIZE, value.getWriteIndex());
+        }
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer3() {
+        doTestSequentialProtonBufferTransfer3(false, false);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer3DirectSource() {
+        doTestSequentialProtonBufferTransfer3(true, false);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer3DirectTargetBuffer() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialProtonBufferTransfer3(false, true);
+    }
+
+    @Test
+    public void testSequentialProtonBufferTransfer3DirectSourceAndTargetBuffers() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestSequentialProtonBufferTransfer3(true, true);
+    }
+
+    private void doTestSequentialProtonBufferTransfer3(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        final byte[] valueContent = new byte[BLOCK_SIZE];
+        final ProtonBuffer value;
+        if (directSource) {
+            value = new ProtonNioByteBuffer(ByteBuffer.allocateDirect(BLOCK_SIZE));
+        } else {
+            value = new ProtonByteBuffer(BLOCK_SIZE, BLOCK_SIZE);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear().writeBytes(valueContent);
+            assertEquals(0, value.getReadIndex());
+            assertEquals(BLOCK_SIZE, value.getWriteIndex());
+            buffer.setWriteIndex(buffer.getWriteIndex() + BLOCK_SIZE);
+            buffer.setBytes(i, value, BLOCK_SIZE);
+            assertEquals(BLOCK_SIZE, value.getReadIndex());
+            assertEquals(BLOCK_SIZE, value.getWriteIndex());
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            value.clear();
+            assertEquals(0, value.getReadIndex());
+            assertEquals(0, value.getWriteIndex());
+            buffer.getBytes(i, value, BLOCK_SIZE);
+            assertEquals(0, value.getReadIndex());
+            assertEquals(BLOCK_SIZE, value.getWriteIndex());
+            for (int j = 0; j < BLOCK_SIZE; j++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+        }
+    }
+
+    //----- Tests need to define these allocation methods
+
+    /**
+     * @return true if the buffer type under test support capacity alterations.
+     */
+    protected boolean canBufferCapacityBeChanged() {
+        return true;
+    }
+
+    /**
+     * @return true if the buffer implementation can allocate a version with a direct buffer backed implementation.
+     */
+    protected abstract boolean canAllocateDirectBackedBuffers();
+
+    /**
+     * @return a ProtonBuffer allocated with defaults for capacity and max-capacity.
+     */
+    protected ProtonBuffer allocateDefaultBuffer() {
+        return allocateBuffer(DEFAULT_CAPACITY);
+    }
+
+    /**
+     * @return a ProtonBuffer allocated with defaults for capacity and max-capacity.
+     */
+    protected ProtonBuffer allocateDefaultDirectBuffer() {
+        return allocateDirectBuffer(DEFAULT_CAPACITY);
+    }
+
+    /**
+     * @param initialCapacity the initial capacity to assign the returned buffer
+     *
+     * @return a ProtonBuffer allocated with the given capacity and a default max-capacity.
+     */
+    protected abstract ProtonBuffer allocateBuffer(int initialCapacity);
+
+    /**
+     * @param initialCapacity the initial capacity to assign the returned buffer
+     *
+     * @return a ProtonBuffer allocated with the given capacity and a default max-capacity.
+     */
+    protected abstract ProtonBuffer allocateDirectBuffer(int initialCapacity);
+
+    /**
+     * @param initialCapacity the initial capacity to assign the returned buffer
+     * @param maxCapacity the maximum capacity the buffer is allowed to grow to
+     *
+     * @return a ProtonBuffer allocated with the given capacity and the given max-capacity.
+     */
+    protected abstract ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity);
+
+    /**
+     * @param initialCapacity the initial capacity to assign the returned buffer
+     * @param maxCapacity the maximum capacity the buffer is allowed to grow to
+     *
+     * @return a ProtonBuffer allocated with the given capacity and the given max-capacity.
+     */
+    protected abstract ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity);
+
+    /**
+     * @param array the byte array to wrap with the given buffer under test.
+     *
+     * @return a ProtonBuffer that wraps the given buffer.
+     */
+    protected abstract ProtonBuffer wrapBuffer(byte[] array);
+
+    //----- Test support methods
+
+    public static void assertRemainingEquals(ByteBuffer expected, ByteBuffer actual) {
+        int remaining1 = expected.remaining();
+        int remaining2 = actual.remaining();
+
+        assertEquals(remaining1, remaining2);
+        byte[] array1 = new byte[remaining1];
+        byte[] array2 = new byte[remaining2];
+        expected.get(array1);
+        actual.get(array2);
+        assertArrayEquals(array1, array2);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonBufferInputStreamTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonBufferInputStreamTest.java
new file mode 100644
index 0000000..b019476
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonBufferInputStreamTest.java
@@ -0,0 +1,384 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+public class ProtonBufferInputStreamTest {
+
+    @Test
+    public void testCannotReadFromClosedStream() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        stream.close();
+
+        assertThrows(IOException.class, () -> stream.read());
+        assertThrows(IOException.class, () -> stream.readLine());
+    }
+
+    @Test
+    public void testBufferWappedExposesAvailableBytes() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadFully() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+
+        final byte[] target = new byte[payload.length];
+        stream.readFully(target);
+        assertEquals(0, stream.available());
+
+        assertArrayEquals(payload, target);
+
+        assertThrows(IOException.class, () -> stream.readFully(target, 1, 1));
+        assertThrows(NullPointerException.class, () -> stream.readFully(null));
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadReturnsMinusOneAfterAllBytesRead() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(stream.read(), payload[i]);
+        }
+
+        assertEquals(-1, stream.read());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadArrayReturnsMinusOneAfterAllBytesRead() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        final byte[] target = new byte[payload.length];
+        assertEquals(payload.length, stream.read(target));
+        assertEquals(-1, stream.read(target));
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadDataFromByteWrittenWithJavaStreams() throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(bos);
+
+        dos.writeInt(1024);
+        dos.write(new byte[] { 0, 1, 2, 3 });
+        dos.writeBoolean(false);
+        dos.writeBoolean(true);
+        dos.writeByte(255);
+        dos.writeByte(254);
+        dos.writeChar(65535);
+        dos.writeShort(32765);
+        dos.writeLong(Long.MAX_VALUE);
+        dos.writeFloat(3.14f);
+        dos.writeDouble(3.14);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(bos.toByteArray());
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+
+        final byte[] sink = new byte[4];
+
+        assertEquals(1024, stream.readInt());
+        stream.read(sink);
+        assertArrayEquals(new byte[] { 0, 1, 2, 3 }, sink);
+        assertEquals(false, stream.readBoolean());
+        assertEquals(true, stream.readBoolean());
+        assertEquals(255, stream.read());
+        assertEquals(254, stream.readUnsignedByte());
+        assertEquals(65535, stream.readChar());
+        assertEquals(32765, stream.readShort());
+        assertEquals(Long.MAX_VALUE, stream.readLong());
+        assertEquals(3.14f, stream.readFloat(), 0.01f);
+        assertEquals(3.14, stream.readDouble(), 0.01);
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadUTF8StringFromDataOuputWrite() throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(bos);
+
+        dos.writeUTF("Hello World");
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(bos.toByteArray());
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+
+        assertEquals("Hello World", stream.readUTF());
+
+        stream.close();
+    }
+
+    @Test
+    public void testMarkReadIndex() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertTrue(stream.markSupported());
+        assertEquals(payload.length, stream.available());
+        assertEquals(payload[0], stream.readByte());
+
+        stream.mark(100);
+
+        assertEquals(payload[1], stream.readByte());
+
+        stream.reset();
+
+        assertEquals(payload[1], stream.readByte());
+        assertEquals(payload[2], stream.readByte());
+
+        stream.close();
+    }
+
+    @Test
+    public void testGetBytesRead() throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(bos);
+
+        dos.writeInt(1024);
+        dos.write(new byte[] { 0, 1, 2, 3 });
+        dos.writeBoolean(false);
+        dos.writeBoolean(true);
+        dos.writeByte(255);
+        dos.writeChar(65535);
+        dos.writeLong(Long.MAX_VALUE);
+        dos.writeFloat(3.14f);
+        dos.writeDouble(3.14);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(bos.toByteArray());
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+
+        final byte[] sink = new byte[4];
+
+        assertEquals(0, stream.getBytesRead());
+        assertEquals(1024, stream.readInt());
+        assertEquals(4, stream.getBytesRead());
+        stream.read(sink);
+        assertArrayEquals(new byte[] { 0, 1, 2, 3 }, sink);
+        assertEquals(8, stream.getBytesRead());
+        assertEquals(false, stream.readBoolean());
+        assertEquals(9, stream.getBytesRead());
+        assertEquals(true, stream.readBoolean());
+        assertEquals(10, stream.getBytesRead());
+        assertEquals(255, stream.read());
+        assertEquals(11, stream.getBytesRead());
+        assertEquals(65535, stream.readChar());
+        assertEquals(13, stream.getBytesRead());
+        assertEquals(Long.MAX_VALUE, stream.readLong());
+        assertEquals(21, stream.getBytesRead());
+        assertEquals(3.14f, stream.readFloat(), 0.01f);
+        assertEquals(25, stream.getBytesRead());
+        assertEquals(3.14, stream.readDouble(), 0.01);
+        assertEquals(33, stream.getBytesRead());
+
+        stream.close();
+    }
+
+    @Test
+    public void testSkip() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(payload.length, stream.skip(Integer.MAX_VALUE));
+        assertEquals(0, stream.available());
+
+        stream.close();
+    }
+
+    @Test
+    public void testSkipLong() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(payload.length, stream.skip(Long.MAX_VALUE));
+        assertEquals(0, stream.available());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineWhenNoneAvailable() throws IOException {
+        final String input = new String("Hello World\n");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        stream.skip(payload.length);
+        assertEquals(0, stream.available());
+        assertNull(stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLines() throws IOException {
+        final String input = new String("Hello World\nThis is a test\n");
+        final String expected1 = new String("Hello World");
+        final String expected2 = new String("This is a test");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected1, stream.readLine());
+        assertEquals(expected2, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLine() throws IOException {
+        final String input = new String("Hello World\n");
+        final String expected = new String("Hello World");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineOnlyNewLine() throws IOException {
+        final String input = new String("\n");
+        final String expected = new String("");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineOnlyCRLF() throws IOException {
+        final String input = new String("\r\n");
+        final String expected = new String("");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineWhenNoNewLineAndNoMoreAvailable() throws IOException {
+        final String input = new String("Hello World");
+        final String expected = new String("Hello World");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineWhenCRIsInStringAlone() throws IOException {
+        final String input = new String("Hello World\rABC\r");
+        final String expected1 = new String("Hello World");
+        final String expected2 = new String("ABC");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected1, stream.readLine());
+        assertEquals(expected2, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineCarriageReturnAndLineFeed() throws IOException {
+        final String input = new String("Hello World\r\n");
+        final String expected = new String("Hello World");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected, stream.readLine());
+
+        stream.close();
+    }
+
+    @Test
+    public void testReadLineCarriageReturnAndLineFeedThenEmptyLine() throws IOException {
+        final String input = new String("Hello World\r\n\r\n");
+        final String expected = new String("Hello World");
+        final byte[] payload = input.getBytes(StandardCharsets.UTF_8);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+        ProtonBufferInputStream stream = new ProtonBufferInputStream(buffer);
+        assertEquals(payload.length, stream.available());
+        assertEquals(expected, stream.readLine());
+        assertEquals("", stream.readLine());
+
+        stream.close();
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonBufferOutputStreamTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonBufferOutputStreamTest.java
new file mode 100644
index 0000000..118e223
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonBufferOutputStreamTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+public class ProtonBufferOutputStreamTest {
+
+    @Test
+    public void testBufferWappedExposesWrittenBytes() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(payload.length);
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+        assertEquals(0, stream.getBytesWritten());
+
+        stream.write(payload);
+
+        assertEquals(payload.length, stream.getBytesWritten());
+
+        stream.close();
+    }
+
+    @Test
+    public void testBufferWritesGivenArrayBytes() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(payload.length);
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+        assertEquals(0, stream.getBytesWritten());
+
+        stream.write(payload);
+
+        for (int i = 0; i < payload.length; ++i) {
+            assertEquals(payload[i], buffer.getByte(i));
+        }
+
+        stream.close();
+    }
+
+    @Test
+    public void testZeroLengthWriteBytesDoesNotWriteOrThrow() throws IOException {
+        byte[] payload = new byte[] { 0, 1, 2, 3, 4, 5 };
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(payload.length);
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+
+        stream.write(payload, 0, 0);
+
+        assertEquals(0, stream.getBytesWritten());
+
+        stream.close();
+    }
+
+    @Test
+    public void testCannotWriteToClosedStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+
+        stream.close();
+        assertThrows(IOException.class, () -> stream.writeInt(1024));
+    }
+
+    @Test
+    public void testWriteValuesAndReadWithDataInputStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+
+        stream.write(32);
+        stream.writeInt(1024);
+        stream.write(new byte[] { 0, 1, 2, 3 });
+        stream.writeBoolean(false);
+        stream.writeBoolean(true);
+        stream.writeByte(255);
+        stream.writeShort(32767);
+        stream.writeLong(Long.MAX_VALUE);
+        stream.writeChar(65535);
+        stream.writeFloat(3.14f);
+        stream.writeDouble(3.14);
+
+        final byte[] array = buffer.toByteBuffer().array();
+        ByteArrayInputStream bis = new ByteArrayInputStream(array, 0, buffer.getReadableBytes());
+        DataInputStream dis = new DataInputStream(bis);
+
+        final byte[] sink = new byte[4];
+
+        assertEquals(32, dis.read());
+        assertEquals(1024, dis.readInt());
+        dis.read(sink);
+        assertArrayEquals(new byte[] { 0, 1, 2, 3 }, sink);
+        assertEquals(false, dis.readBoolean());
+        assertEquals(true, dis.readBoolean());
+        assertEquals(255, dis.read());
+        assertEquals(32767, dis.readShort());
+        assertEquals(Long.MAX_VALUE, dis.readLong());
+        assertEquals(65535, dis.readChar());
+        assertEquals(3.14f, dis.readFloat(), 0.01f);
+        assertEquals(3.14, dis.readDouble(), 0.01);
+
+        stream.close();
+    }
+
+    @Test
+    public void testWriteChars() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+        String expected = "Hello World";
+
+        stream.writeChars(expected);
+
+        final byte[] array = buffer.toByteBuffer().array();
+        ByteArrayInputStream bis = new ByteArrayInputStream(array, 0, buffer.getReadableBytes());
+        DataInputStream dis = new DataInputStream(bis);
+
+        for (char letter : expected.toCharArray()) {
+            assertEquals(letter, dis.readChar());
+        }
+
+        stream.close();
+    }
+
+    @Test
+    public void testWriteUtf8StringAndReadWithDataInputStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+
+        stream.writeUTF("Hello World");
+        stream.writeUTF("Hello World Again");
+
+        final byte[] array = buffer.toByteBuffer().array();
+        ByteArrayInputStream bis = new ByteArrayInputStream(array, 0, buffer.getReadableBytes());
+        DataInputStream dis = new DataInputStream(bis);
+
+        assertEquals("Hello World", dis.readUTF());
+        assertEquals("Hello World Again", dis.readUTF());
+
+        stream.close();
+    }
+
+    @Test
+    public void testWriteBytesFromString() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        ProtonBufferOutputStream stream = new ProtonBufferOutputStream(buffer);
+
+        stream.writeBytes("Hello World");
+
+        final byte[] array = buffer.toByteBuffer().array();
+        ByteArrayInputStream bis = new ByteArrayInputStream(array, 0, buffer.getReadableBytes());
+        DataInputStream dis = new DataInputStream(bis);
+
+        byte[] result = dis.readAllBytes();
+
+        assertArrayEquals(result, "Hello World".getBytes(StandardCharsets.US_ASCII));
+
+        stream.close();
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferAllocatorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferAllocatorTest.java
new file mode 100644
index 0000000..6891cec
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferAllocatorTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.nio.ByteBuffer;
+
+import org.junit.jupiter.api.Test;
+
+public class ProtonByteBufferAllocatorTest {
+
+    @Test
+    public void testAllocate() {
+        ProtonByteBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        assertNotNull(buffer);
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testAllocateWithInitialCapacity() {
+        ProtonByteBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(1024);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(1024, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testAllocateWithInvalidInitialCapacity() {
+        try {
+            ProtonByteBufferAllocator.DEFAULT.allocate(-1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAllocateWithInitialAndMaximumCapacity() {
+        ProtonByteBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(1024, 2048);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertNotEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertEquals(1024, buffer.capacity());
+        assertEquals(2048, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testAllocateWithInvalidInitialAndMaximimCapacity() {
+        try {
+            ProtonByteBufferAllocator.DEFAULT.allocate(64, 32);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            ProtonByteBufferAllocator.DEFAULT.allocate(-1, 64);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            ProtonByteBufferAllocator.DEFAULT.allocate(-1, -1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            ProtonByteBufferAllocator.DEFAULT.allocate(64, -1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testOutputBufferWithInitialCapacity() {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.outputBuffer(1024);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(1024, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testOutputBufferWithInitialAndMaximumCapacity() {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.outputBuffer(1024, 2048);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertNotEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertEquals(1024, buffer.capacity());
+        assertEquals(2048, buffer.maxCapacity());
+    }
+
+    @Test
+    public void testWrapByteArray() {
+        byte[] source = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(source);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertNotEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertEquals(source.length, buffer.capacity());
+        assertEquals(source.length, buffer.maxCapacity());
+
+        assertSame(source, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testWrapByteArrayWithOffsetAndLength() {
+        byte[] source = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(source, 0, source.length);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertNotEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.capacity());
+
+        assertEquals(source.length, buffer.capacity());
+        assertEquals(source.length, buffer.maxCapacity());
+
+        assertSame(source, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testWrapByteArrayWithOffsetAndLengthSubset() {
+        byte[] source = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(source, 1, source.length - 2);
+
+        assertNotNull(buffer);
+        assertNotEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertNotEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.capacity());
+
+        assertEquals(source.length - 2, buffer.capacity());
+        assertEquals(source.length - 2, buffer.maxCapacity());
+
+        assertSame(source, buffer.getArray());
+        assertEquals(1, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testCannotWrapReadOnlyByteBuffer() {
+        ByteBuffer buffer = ByteBuffer.allocate(1024).asReadOnlyBuffer();
+        assertThrows(UnsupportedOperationException.class, () -> ProtonByteBufferAllocator.DEFAULT.wrap(buffer));
+    }
+
+    @Test
+    public void testCannotWrapByteBufferWithoutArrayBacking() {
+        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
+
+        assumeTrue(buffer.isDirect());
+        assumeFalse(buffer.hasArray());
+
+        assertThrows(UnsupportedOperationException.class, () -> ProtonByteBufferAllocator.DEFAULT.wrap(buffer));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferSliceTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferSliceTest.java
new file mode 100644
index 0000000..2d07f83
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferSliceTest.java
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for the ProtonByteBufferSlice class
+ */
+public class ProtonByteBufferSliceTest {
+
+    //----- Test Slice creation ----------------------------------------------//
+
+    @Test
+    public void testCreateEmptySlice() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        ProtonBuffer slice = buffer.slice();
+
+        assertEquals(0, slice.getReadableBytes());
+        assertEquals(0, slice.capacity());
+        assertEquals(0, slice.maxCapacity());
+
+        assertTrue(slice.hasArray());
+        assertNotNull(slice.getArray());
+        assertEquals(0, slice.getArrayOffset());
+    }
+
+    @Test
+    public void testCreateSlice() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeBytes(new byte[] {0, 1, 2, 3, 4, 5});
+        buffer.setReadIndex(1);
+
+        assertEquals(5, buffer.getReadableBytes());
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        ProtonBuffer slice = buffer.slice();
+
+        assertEquals(5, slice.getReadableBytes());
+        assertEquals(5, slice.capacity());
+        assertEquals(5, slice.maxCapacity());
+
+        assertTrue(slice.hasArray());
+        assertNotNull(slice.getArray());
+        assertEquals(1, slice.getArrayOffset());
+
+        assertEquals(1, slice.readByte());
+    }
+
+    @Test
+    public void testCreateSliceOfASlice() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeBytes(new byte[] {0, 1, 2, 3, 4, 5});
+        buffer.setReadIndex(1);
+
+        assertEquals(5, buffer.getReadableBytes());
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        ProtonBuffer slice = buffer.slice();
+        slice.readByte();
+
+        ProtonBuffer sliceOfSlice = slice.slice();
+
+        assertEquals(4, sliceOfSlice.getReadableBytes());
+        assertEquals(4, sliceOfSlice.capacity());
+        assertEquals(4, sliceOfSlice.maxCapacity());
+
+        assertTrue(sliceOfSlice.hasArray());
+        assertNotNull(sliceOfSlice.getArray());
+        assertEquals(2, sliceOfSlice.getArrayOffset());
+
+        assertEquals(2, sliceOfSlice.readByte());
+    }
+
+    @Test
+    public void testCreateSliceByIndex() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeBytes(new byte[] {0, 1, 2, 3, 4, 5});
+
+        ProtonBuffer slice = buffer.slice(1, 4);
+
+        assertEquals(4, slice.getReadableBytes());
+        assertEquals(4, slice.capacity());
+        assertEquals(4, slice.maxCapacity());
+
+        assertTrue(slice.hasArray());
+        assertNotNull(slice.getArray());
+        assertEquals(1, slice.getArrayOffset());
+
+        assertEquals(1, slice.readByte());
+    }
+
+    @Test
+    public void testCreateSliceByIndexBoundsChecks() {
+        ProtonBuffer buffer = new ProtonByteBuffer(6, 6);
+
+        buffer.writeBytes(new byte[] {0, 1, 2, 3, 4, 5});
+
+        try {
+            buffer.slice(1, 6);
+            fail("Should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.slice(-1, 5);
+            fail("Should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.slice(1, -5);
+            fail("Should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+
+        try {
+            buffer.slice(-1, -5);
+            fail("Should have thrown IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException iobe) {}
+    }
+
+    //----- Test capacity alteration -----------------------------------------//
+
+    @Test
+    public void testCapacityUpdatesNotAllowed() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeBytes(new byte[] {0, 1, 2, 3, 4, 5});
+
+        ProtonBuffer slice = buffer.slice();
+
+        try {
+            slice.capacity(65535);
+            fail("Should not be able to alter capacity");
+        } catch (UnsupportedOperationException uoe) {}
+
+        try {
+            slice.capacity(buffer.capacity());
+            fail("Should not be able to alter capacity");
+        } catch (UnsupportedOperationException uoe) {}
+    }
+
+    //----- Read Primitives Tests -------------------------------------------//
+
+    @Test
+    public void testReadByte() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeByte((byte) 0);
+        buffer.writeByte((byte) 56);
+
+        ProtonBuffer slice = buffer.slice(1, 1);
+
+        assertEquals(1, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(56, slice.readByte());
+
+        assertEquals(1, slice.getWriteIndex());
+        assertEquals(1, slice.getReadIndex());
+
+        assertEquals(0, slice.getReadableBytes());
+    }
+
+    @Test
+    public void testReadBoolean() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeBoolean(true);
+        buffer.writeBoolean(false);
+
+        ProtonBuffer slice = buffer.slice(1, 1);
+
+        assertEquals(1, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(false, slice.readBoolean());
+
+        assertEquals(1, slice.getWriteIndex());
+        assertEquals(1, slice.getReadIndex());
+    }
+
+    @Test
+    public void testReadShort() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeShort((short) 0);
+        buffer.writeShort((short) 42);
+
+        ProtonBuffer slice = buffer.slice(2, 2);
+
+        assertEquals(2, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(42, slice.readShort());
+
+        assertEquals(2, slice.getWriteIndex());
+        assertEquals(2, slice.getReadIndex());
+
+        assertEquals(0, slice.getReadableBytes());
+    }
+
+    @Test
+    public void testWriteInt() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeInt(0);
+        buffer.writeInt(72);
+
+        ProtonBuffer slice = buffer.slice(4, 4);
+
+        assertEquals(4, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(72, slice.readInt());
+
+        assertEquals(4, slice.getWriteIndex());
+        assertEquals(4, slice.getReadIndex());
+
+        assertEquals(0, slice.getReadableBytes());
+    }
+
+    @Test
+    public void testReadLong() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeLong(0l);
+        buffer.writeLong(500l);
+
+        ProtonBuffer slice = buffer.slice(8, 8);
+
+        assertEquals(8, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(500l, slice.readLong());
+
+        assertEquals(8, slice.getWriteIndex());
+        assertEquals(8, slice.getReadIndex());
+
+        assertEquals(0, slice.getReadableBytes());
+    }
+
+    @Test
+    public void testReadFloat() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeFloat(1.1f);
+        buffer.writeFloat(35.5f);
+
+        ProtonBuffer slice = buffer.slice(4, 4);
+
+        assertEquals(4, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(35.5f, slice.readFloat(), 0.4f);
+
+        assertEquals(4, slice.getWriteIndex());
+        assertEquals(4, slice.getReadIndex());
+
+        assertEquals(0, slice.getReadableBytes());
+    }
+
+    @Test
+    public void testReadDouble() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        buffer.writeDouble(2.68);
+        buffer.writeDouble(1.66);
+
+        ProtonBuffer slice = buffer.slice(8, 8);
+
+        assertEquals(8, slice.getWriteIndex());
+        assertEquals(0, slice.getReadIndex());
+
+        assertEquals(1.66, slice.readDouble(), 0.1);
+
+        assertEquals(8, slice.getWriteIndex());
+        assertEquals(8, slice.getReadIndex());
+
+        assertEquals(0, slice.getReadableBytes());
+    }
+
+}
\ No newline at end of file
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferTest.java
new file mode 100644
index 0000000..e73a1c6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonByteBufferTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.util.ProtonTestByteBuffer;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test behavior of the built in ProtonByteBuffer implementation.
+ */
+public class ProtonByteBufferTest extends ProtonAbstractBufferTest {
+
+    //----- Test Buffer creation ---------------------------------------------//
+
+    @Test
+    public void testNullOnUnwrap() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+        assertNull(buffer.unwrap());
+    }
+
+    @Test
+    public void testDefaultConstructor() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testConstructorCapacityAndMaxCapacityAllocatesArray() {
+        int baseCapaity = ProtonByteBuffer.DEFAULT_CAPACITY + 10;
+        ProtonBuffer buffer = new ProtonByteBuffer(baseCapaity, baseCapaity + 100);
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(baseCapaity, buffer.capacity());
+        assertEquals(baseCapaity + 100, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testConstructorCapacityExceptions() {
+        assertThrows(IllegalArgumentException.class, () -> new ProtonByteBuffer(-1));
+    }
+
+    @Test
+    public void testConstructorCapacityMaxCapacity() {
+        ProtonBuffer buffer = new ProtonByteBuffer(
+            ProtonByteBuffer.DEFAULT_CAPACITY + 10, ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY - 100);
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY + 10, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY - 100, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testConstructorCapacityMaxCapacityExceptions() {
+
+        try {
+            new ProtonByteBuffer(-1, ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            new ProtonByteBuffer(ProtonByteBuffer.DEFAULT_CAPACITY, -1);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            new ProtonByteBuffer(100, 10);
+            fail("Should have thrown an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testConstructorByteArray() {
+        byte[] source = new byte[ProtonByteBuffer.DEFAULT_CAPACITY + 10];
+
+        ProtonBuffer buffer = new ProtonByteBuffer(source);
+
+        assertEquals(source.length, buffer.getReadableBytes());
+        assertEquals(ProtonByteBuffer.DEFAULT_CAPACITY + 10, buffer.capacity());
+        assertEquals(ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, buffer.maxCapacity());
+
+        assertTrue(buffer.hasArray());
+        assertSame(source, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testConstructorByteArrayThrowsWhenNull() {
+        try {
+            new ProtonByteBuffer(null);
+            fail("Should throw NullPointerException");
+        } catch (NullPointerException npe) {}
+
+        try {
+            new ProtonTestByteBuffer(null, 1);
+            fail("Should throw NullPointerException");
+        } catch (NullPointerException npe) {}
+
+        try {
+            new ProtonTestByteBuffer(null, 1, 1);
+            fail("Should throw NullPointerException");
+        } catch (NullPointerException npe) {}
+    }
+
+    //----- Tests for altering buffer capacity -------------------------------//
+
+    @Test
+    public void testIncreaseCapacityReallocatesArray() {
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = new ProtonByteBuffer(source);
+        assertEquals(100, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertSame(source, buffer.getArray());
+
+        buffer.capacity(200);
+        assertEquals(200, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertNotSame(source, buffer.getArray());
+
+        source = buffer.getArray();
+
+        buffer.capacity(200);
+        assertEquals(200, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertSame(source, buffer.getArray());
+    }
+
+    @Test
+    public void testDecreaseCapacityReallocatesArray() {
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = new ProtonByteBuffer(source);
+        assertEquals(100, buffer.capacity());
+        assertEquals(100, buffer.getWriteIndex());
+        assertTrue(buffer.hasArray());
+        assertSame(source, buffer.getArray());
+
+        buffer.capacity(50);
+        assertEquals(50, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertNotSame(source, buffer.getArray());
+
+        // Buffer is truncated but we never read anything so read index stays at front.
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(50, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testDecreaseCapacityWithReadIndexIndexBeyondNewValueReallocatesArray() {
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = new ProtonByteBuffer(source);
+        assertEquals(100, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertSame(source, buffer.getArray());
+
+        buffer.setReadIndex(60);
+
+        buffer.capacity(50);
+        assertEquals(50, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertNotSame(source, buffer.getArray());
+
+        // Buffer should be truncated and read index moves back to end
+        assertEquals(50, buffer.getReadIndex());
+        assertEquals(50, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testDecreaseCapacityWithWriteIndexWithinNewValueReallocatesArray() {
+        byte[] source = new byte[100];
+
+        ProtonBuffer buffer = new ProtonByteBuffer(source);
+        assertEquals(100, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertSame(source, buffer.getArray());
+
+        buffer.setIndex(10, 30);
+
+        buffer.capacity(50);
+        assertEquals(50, buffer.capacity());
+        assertTrue(buffer.hasArray());
+        assertNotSame(source, buffer.getArray());
+
+        // Buffer should be truncated but index values remain unchanged
+        assertEquals(10, buffer.getReadIndex());
+        assertEquals(30, buffer.getWriteIndex());
+    }
+
+    @Test
+    public void testCapacityIncreasesWhenWritesExceedCurrentReallocatesArray() {
+        ProtonBuffer buffer = new ProtonByteBuffer(10);
+
+        assertTrue(buffer.hasArray());
+
+        assertEquals(10, buffer.capacity());
+        assertEquals(10, buffer.getArray().length);
+        assertEquals(Integer.MAX_VALUE, buffer.maxCapacity());
+
+        for (int i = 1; i <= 9; ++i) {
+            buffer.writeByte(i);
+        }
+
+        assertEquals(10, buffer.capacity());
+
+        buffer.writeByte(10);
+
+        assertEquals(10, buffer.capacity());
+        assertEquals(10, buffer.getArray().length);
+
+        buffer.writeByte(11);
+
+        assertTrue(buffer.capacity() > 10);
+        assertTrue(buffer.getArray().length > 10);
+
+        assertEquals(11, buffer.getReadableBytes());
+
+        for (int i = 1; i < 12; ++i) {
+            assertEquals(i, buffer.readByte());
+        }
+    }
+
+    //----- Tests for Copy operations ----------------------------------------//
+
+    @Test
+    public void testCopyEmptyBufferCopiesBackingArray() {
+        ProtonBuffer buffer = new ProtonByteBuffer(10);
+        ProtonBuffer copy = buffer.copy();
+
+        assertEquals(buffer.getReadableBytes(), copy.getReadableBytes());
+
+        assertTrue(copy.hasArray());
+        assertNotNull(copy.getArray());
+
+        assertNotSame(buffer.getArray(), copy.getArray());
+    }
+
+    @Test
+    public void testCopyBufferResultsInMatchingBackingArrays() {
+        ProtonBuffer buffer = new ProtonByteBuffer(10);
+
+        buffer.writeByte(1);
+        buffer.writeByte(2);
+        buffer.writeByte(3);
+        buffer.writeByte(4);
+        buffer.writeByte(5);
+
+        ProtonBuffer copy = buffer.copy();
+
+        assertEquals(buffer.getReadableBytes(), copy.getReadableBytes());
+
+        assertTrue(copy.hasArray());
+        assertNotNull(copy.getArray());
+
+        assertNotSame(buffer.getArray(), copy.getArray());
+
+        for(int i = 0; i < 5; ++i) {
+            assertEquals(buffer.getArray()[i], copy.getArray()[i]);
+        }
+    }
+
+    //----- Tests for Buffer duplication -------------------------------------//
+
+    @Test
+    public void testDuplicateEmptyBufferRetainsBackingArrayAccess() {
+        ProtonBuffer buffer = new ProtonByteBuffer(10);
+        ProtonBuffer duplicate = buffer.duplicate();
+
+        assertEquals(buffer.capacity(), duplicate.capacity());
+        assertEquals(buffer.getReadableBytes(), duplicate.getReadableBytes());
+
+        assertSame(buffer.getArray(), duplicate.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    //----- Tests for conversion to ByteBuffer -------------------------------//
+
+    @Test
+    public void testToByteBufferWithDataPresentRetainsBackingArray() {
+        ProtonBuffer buffer = new ProtonByteBuffer(10);
+
+        buffer.writeByte(1);
+        buffer.writeByte(2);
+        buffer.writeByte(3);
+        buffer.writeByte(4);
+        buffer.writeByte(5);
+
+        ByteBuffer byteBuffer = buffer.toByteBuffer();
+
+        assertEquals(buffer.getReadableBytes(), byteBuffer.limit());
+
+        assertTrue(byteBuffer.hasArray());
+        assertNotNull(byteBuffer.array());
+
+        assertSame(buffer.getArray(), byteBuffer.array());
+    }
+
+    @Test
+    public void testToByteBufferWhenNoDataRetainsBackingArray() {
+        ProtonBuffer buffer = new ProtonByteBuffer();
+        ByteBuffer byteBuffer = buffer.toByteBuffer();
+
+        assertEquals(buffer.getReadableBytes(), byteBuffer.limit());
+
+        assertTrue(byteBuffer.hasArray());
+        assertNotNull(byteBuffer.array());
+        assertSame(buffer.getArray(), byteBuffer.array());
+    }
+
+    //----- Tests for string conversion --------------------------------------//
+
+    @Test
+    public void testToStringFromUTF8WithNonArrayBackedBuffer() throws Exception {
+        String sourceString = "Test-String-1";
+
+        ProtonTestByteBuffer buffer = new ProtonTestByteBuffer(false);
+        buffer.writeBytes(sourceString.getBytes(StandardCharsets.UTF_8));
+
+        assertFalse(buffer.hasArray());
+
+        String decoded = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(sourceString, decoded);
+    }
+
+    //----- Buffer creation implementation required by super-class
+
+    @Override
+    protected boolean canAllocateDirectBackedBuffers() {
+        return false;
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity) {
+        return new ProtonByteBuffer(initialCapacity);
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonByteBuffer(initialCapacity, maxCapacity);
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected ProtonBuffer wrapBuffer(byte[] array) {
+        return ProtonByteBufferAllocator.DEFAULT.wrap(array);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonCompositeBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonCompositeBufferTest.java
new file mode 100644
index 0000000..e0afa52
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonCompositeBufferTest.java
@@ -0,0 +1,2134 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test the Proton Composite Buffer class
+ */
+public class ProtonCompositeBufferTest extends ProtonAbstractBufferTest {
+
+    @Test
+    public void testCreateDefaultCompositeBuffer() {
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer(Integer.MAX_VALUE);
+        assertNotNull(composite);
+        assertEquals(0, composite.capacity());
+        assertEquals(Integer.MAX_VALUE, composite.maxCapacity());
+    }
+
+    //----- Read and Write Index handling tests
+
+    @Test
+    public void testManipulateReadIndexWithOneArrayAppended() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer(Integer.MAX_VALUE);
+
+        buffer.append(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }));
+
+        assertEquals(10, buffer.capacity());
+        assertEquals(10, buffer.getWriteIndex());
+        assertEquals(0, buffer.getReadIndex());
+
+        buffer.setReadIndex(5);
+        assertEquals(5, buffer.getReadIndex());
+
+        buffer.setReadIndex(6);
+        assertEquals(6, buffer.getReadIndex());
+
+        buffer.setReadIndex(10);
+        assertEquals(10, buffer.getReadIndex());
+
+        try {
+            buffer.setReadIndex(11);
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+
+        buffer.markReadIndex();
+
+        buffer.setReadIndex(0);
+        assertEquals(0, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testPositionWithTwoArraysAppended() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer(Integer.MAX_VALUE);
+
+        buffer.append(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4 }))
+              .append(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 5, 6, 7, 8, 9 }));
+
+        assertEquals(10, buffer.capacity());
+        assertEquals(10, buffer.getReadableBytes());
+
+        buffer.setReadIndex(5);
+        assertEquals(5, buffer.getReadIndex());
+
+        buffer.setReadIndex(6);
+        assertEquals(6, buffer.getReadIndex());
+
+        buffer.setReadIndex(10);
+        assertEquals(10, buffer.getReadIndex());
+
+        try {
+            buffer.setReadIndex(11);
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+
+        buffer.setReadIndex(0);
+        assertEquals(0, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testPositionEnforcesPreconditions() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer(Integer.MAX_VALUE);
+
+        // test with nothing appended.
+        try {
+            buffer.setReadIndex(2);
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+
+        try {
+            buffer.setReadIndex(-1);
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+
+        // Test with something appended
+        buffer.append(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 127 }));
+
+        try {
+            buffer.setReadIndex(2);
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+
+        try {
+            buffer.setReadIndex(-1);
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    //----- Test reading from composite of multiple buffers
+
+    @Test
+    public void testGetByteWithManyArraysWithOneElements() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] {0})
+              .append(new byte[] {1})
+              .append(new byte[] {2})
+              .append(new byte[] {3})
+              .append(new byte[] {4})
+              .append(new byte[] {5})
+              .append(new byte[] {6})
+              .append(new byte[] {7})
+              .append(new byte[] {8})
+              .append(new byte[] {9});
+
+        assertEquals(10, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0; i < 10; ++i) {
+            assertEquals(i, buffer.readByte());
+        }
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(10, buffer.getReadIndex());
+        assertEquals(10, buffer.getWriteIndex());
+
+        try {
+            buffer.readByte();
+            fail("Should not be able to read past end");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testGetByteWithManyArraysWithVariedElements() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] {0})
+              .append(new byte[] {1, 2})
+              .append(new byte[] {3, 4, 5})
+              .append(new byte[] {6})
+              .append(new byte[] {7, 8, 9});
+
+        assertEquals(10, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(10, buffer.getWriteIndex());
+
+        for (int i = 0; i < 10; ++i) {
+            assertEquals(i, buffer.readByte());
+        }
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertEquals(10, buffer.getReadIndex());
+
+        try {
+            buffer.readByte();
+            fail("Should not be able to read past end");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testGetShortByteWithNothingAppended() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        try {
+            buffer.readShort();
+            fail("Should throw a IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testGetShortWithTwoArraysContainingOneElement() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] {8}).append(new byte[] {0});
+
+        assertEquals(2, buffer.getReadableBytes());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(2048, buffer.readShort());
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertFalse(buffer.isReadable());
+        assertEquals(2, buffer.getReadIndex());
+
+        try {
+            buffer.readShort();
+            fail("Should not be able to read past end");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testGetIntWithTwoArraysContainingOneElement() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] { 0 ,0 }).append(new byte[] { 8, 0 });
+
+        assertEquals(4, buffer.getReadableBytes());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(2048, buffer.readInt());
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertFalse(buffer.isReadable());
+        assertEquals(4, buffer.getReadIndex());
+
+        try {
+            buffer.readInt();
+            fail("Should not be able to read past end");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testGetLongWithTwoArraysContainingOneElement() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] { 0 ,0, 0, 0 }).append(new byte[] { 0, 0, 8, 0 });
+
+        assertEquals(8, buffer.getReadableBytes());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        assertEquals(2048, buffer.readLong());
+
+        assertEquals(0, buffer.getReadableBytes());
+        assertFalse(buffer.isReadable());
+        assertEquals(8, buffer.getReadIndex());
+
+        try {
+            buffer.readLong();
+            fail("Should not be able to read past end");
+        } catch (IndexOutOfBoundsException e) {}
+    }
+
+    @Test
+    public void testGetWritableBufferWithContentsInSeveralArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {0, 1, 2, 3, 4};
+        byte[] data2 = new byte[] {5, 6, 7, 8, 9};
+        byte[] data3 = new byte[] {10, 11, 12};
+
+        int size = data1.length + data2.length + data3.length;
+
+        buffer.append(data1).append(data2).append(data3);
+
+        assertEquals(size, buffer.getWriteIndex());
+
+        ProtonBuffer destination = ProtonByteBufferAllocator.DEFAULT.allocate(1, 1);
+
+        for (int i = 0; i < size; i++) {
+            assertEquals(buffer.getReadIndex(), i);
+            ProtonBuffer self = buffer.readBytes(destination);
+            assertEquals(destination.getByte(0), buffer.getByte(i));
+            assertSame(self, buffer);
+            destination.setWriteIndex(0);
+        }
+
+        try {
+            buffer.readBytes(destination);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        try {
+            buffer.readBytes((ProtonBuffer) null);
+            fail("Should throw NullPointerException");
+        } catch (NullPointerException e) {
+        }
+    }
+
+    @Test
+    public void testGetintWithContentsInMultipleArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] {0, 1, 2, 3, 4}).append(new byte[] {5, 6, 7, 8, 9});
+
+        for (int i = 0; i < buffer.capacity(); i++) {
+            assertEquals(buffer.getReadIndex(), i);
+            assertEquals(buffer.readByte(), buffer.getByte(i));
+        }
+
+        try {
+            buffer.getByte(-1);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        try {
+            buffer.getByte(buffer.getWriteIndex());
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+    }
+
+    @Test
+    public void testGetbyteArrayIntIntWithContentsInMultipleArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {0, 1, 2, 3, 4};
+        byte[] data2 = new byte[] {5, 6, 7, 8, 9};
+
+        final int dataLength = data1.length + data2.length;
+
+        buffer.append(data1).append(data2);
+
+        assertEquals(dataLength, buffer.getReadableBytes());
+
+        byte array[] = new byte[buffer.getReadableBytes()];
+
+        try {
+            buffer.readBytes(new byte[dataLength + 1], 0, dataLength + 1);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        assertEquals(buffer.getReadIndex(), 0);
+
+        try {
+            buffer.readBytes(array, -1, array.length);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        buffer.readBytes(array, array.length, 0);
+
+        try {
+            buffer.readBytes(array, array.length + 1, 1);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        assertEquals(buffer.getReadIndex(), 0);
+
+        try {
+            buffer.readBytes(array, 2, -1);
+            fail("Should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+        }
+
+        try {
+            buffer.readBytes(array, 2, array.length);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        try {
+            buffer.readBytes((byte[])null, -1, 0);
+            fail("Should throw NullPointerException");
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            buffer.readBytes(array, 1, Integer.MAX_VALUE);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        try {
+            buffer.readBytes(array, Integer.MAX_VALUE, 1);
+            fail("Should throw IndexOutOfBoundsException");
+        } catch (IndexOutOfBoundsException e) {
+        }
+
+        assertEquals(buffer.getReadIndex(), 0);
+
+        ProtonBuffer self = buffer.readBytes(array, 0, array.length);
+        assertEquals(buffer.getReadIndex(), buffer.capacity());
+        assertContentEquals(buffer, array, 0, array.length);
+        assertSame(self, buffer);
+    }
+
+    @Test
+    public void testSetAndGetShortAcrossMultipleArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final int NUM_ELEMENTS = 4;
+
+        for (int i = 0; i < Short.BYTES * NUM_ELEMENTS; ++i) {
+            buffer.append(new byte[] {0});
+        }
+
+        for (int i = 0, j = 1; i < buffer.getReadableBytes(); i += Short.BYTES, j++) {
+            buffer.setShort(i, j);
+        }
+
+        assertEquals(Short.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0, j = 1; i < buffer.getReadableBytes(); i += Short.BYTES, j++) {
+            assertEquals(j, buffer.getShort(i));
+        }
+
+        assertEquals(Short.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(Short.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testSetAndGetIntegersAcrossMultipleArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final int NUM_ELEMENTS = 4;
+
+        for (int i = 0; i < Integer.BYTES * NUM_ELEMENTS; ++i) {
+            buffer.append(new byte[] {0});
+        }
+
+        for (int i = 0, j = 1; i < buffer.getReadableBytes(); i += Integer.BYTES, j++) {
+            buffer.setShort(i, j);
+        }
+
+        assertEquals(Integer.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0, j = 1; i < buffer.getReadableBytes(); i += Integer.BYTES, j++) {
+            assertEquals(j, buffer.getShort(i));
+        }
+
+        assertEquals(Integer.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(Integer.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+    }
+
+    @Test
+    public void testSetAndGetLongsAcrossMultipleArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final int NUM_ELEMENTS = 4;
+
+        for (int i = 0; i < Long.BYTES * NUM_ELEMENTS; ++i) {
+            buffer.append(new byte[] {0});
+        }
+
+        for (int i = 0, j = 1; i < buffer.getReadableBytes(); i += Long.BYTES, j++) {
+            buffer.setShort(i, j);
+        }
+
+        assertEquals(Long.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+
+        for (int i = 0, j = 1; i < buffer.getReadableBytes(); i += Long.BYTES, j++) {
+            assertEquals(j, buffer.getShort(i));
+        }
+
+        assertEquals(Long.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(Long.BYTES * NUM_ELEMENTS, buffer.getReadableBytes());
+    }
+
+    //----- Test array access method
+
+    @Test
+    public void testArrayWhenEmpty() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        assertNotNull(buffer.getArray());
+        assertSame(buffer.getArray(), buffer.getArray());
+
+        byte[] data1 = new byte[] {0, 1, 2, 3, 4};
+
+        buffer.append(data1, 1, data1.length - 1);
+        assertEquals(1, buffer.getArrayOffset());
+
+        assertEquals(data1, buffer.getArray());
+    }
+
+    @Test
+    public void testArrayUnsupportedWhenCompositeHasMultipleChunks() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {0, 1, 2, 3, 4};
+        byte[] data2 = new byte[] {5, 6, 7, 8, 9};
+
+        final int dataLength = data1.length + data2.length;
+
+        buffer.append(data1).append(data2);
+        assertEquals(dataLength, buffer.getReadableBytes());
+
+        try {
+            buffer.getArray();
+            fail("Should not be able to get an array after more than one array added");
+        } catch (UnsupportedOperationException uoe) {}
+    }
+
+    //----- Test arrayOffset method ------------------------------------------//
+
+    @Test
+    public void testArrayOffsetZeroWhenNoChunksInBuffer() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        assertEquals(0, buffer.getArrayOffset());
+
+        byte[] data1 = new byte[] {0, 1, 2, 3, 4};
+
+        buffer.append(data1, 1, data1.length - 1);
+        assertEquals(1, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testArrayOffsetUnsupportedWhenCompositeHasMultipleChunks() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {0, 1, 2, 3, 4};
+        byte[] data2 = new byte[] {5, 6, 7, 8, 9};
+
+        final int dataLength = data1.length + data2.length;
+
+        buffer.append(data1).append(data2);
+        assertEquals(dataLength, buffer.getReadableBytes());
+
+        try {
+            buffer.getArrayOffset();
+            fail("Should not be able to get an offset after more than one array added");
+        } catch (UnsupportedOperationException uoe) {}
+    }
+
+    @Test
+    public void testArrayOffsetIsZeroRegardlessOfPositionOnNonSlicedBuffer() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] data = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer.append(data);
+
+        assertTrue(buffer.hasArray());
+        assertSame(data, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        buffer.setReadIndex(1);
+
+        assertSame(data, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        buffer.setReadIndex(buffer.getReadableBytes());
+
+        assertSame(data, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        buffer.setReadIndex(0);
+
+        assertSame(data, buffer.getArray());
+        assertEquals(0, buffer.getArrayOffset());
+    }
+
+    @Test
+    public void testArrayOffsetIsFixedOnSliceRegardlessOfPosition() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] data = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer.append(data);
+
+        assertTrue(buffer.hasArray());
+        assertEquals(0, buffer.getArrayOffset());
+
+        buffer.setReadIndex(1);
+        ProtonBuffer slice = buffer.slice();
+
+        assertEquals(1, slice.getArrayOffset());
+
+        slice.setReadIndex(slice.getReadableBytes());
+
+        assertEquals(1, slice.getArrayOffset());
+
+        slice.setReadIndex(0);
+
+        assertEquals(1, slice.getArrayOffset());
+
+        slice.setReadIndex(1);
+
+        ProtonBuffer anotherSlice = slice.slice();
+
+        assertEquals(2, anotherSlice.getArrayOffset());
+    }
+
+    @Test
+    public void testArrayOffset() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        assertTrue(buffer.hasArray());
+        assertEquals(0, buffer.getArrayOffset());
+        assertNotNull(buffer.getArray());
+        assertEquals(0, buffer.getArray().length);
+
+        buffer.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+
+        assertTrue(buffer.hasArray());
+        assertEquals(0, buffer.getArrayOffset(), "Unexpected array offset");
+    }
+
+    @Test
+    public void testArrayOffsetAfterDuplicate() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+
+        assertEquals(0, buffer.readByte(), "Unexpected get result");
+
+        ProtonBuffer duplicate = buffer.duplicate();
+
+        assertTrue(duplicate.hasArray());
+        assertEquals(0, duplicate.getArrayOffset(), "Unexpected array offset after duplication");
+
+        assertEquals(1, duplicate.readByte(), "Unexpected get result");
+
+        assertEquals(0, duplicate.getArrayOffset(), "Unexpected array offset after duplicate use");
+        assertEquals(2, duplicate.readByte(), "Unexpected get result");
+
+        assertEquals(0, buffer.getArrayOffset(), "Unexpected array offset on original");
+    }
+
+    @Test
+    public void testArrayOffsetAfterSliceDuplicated() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+
+        assertEquals(0, buffer.readByte(), "Unexpected get result");
+
+        ProtonBuffer slice = buffer.slice();
+        ProtonBuffer sliceDuplicated = slice.duplicate();
+
+        assertTrue(sliceDuplicated.hasArray());
+        assertEquals(0, sliceDuplicated.getArrayOffset(), "Unexpected array offset after duplication");
+
+        assertEquals(1, sliceDuplicated.readByte(), "Unexpected get result");
+
+        assertEquals(0, sliceDuplicated.getArrayOffset(), "Unexpected array offset after duplicate use");
+        assertEquals(2, sliceDuplicated.readByte(), "Unexpected get result");
+
+        assertEquals(0, buffer.getArrayOffset(), "Unexpected array offset on original");
+        assertEquals(1, slice.getArrayOffset(), "Unexpected array offset on slice");
+    }
+
+    //----- Test appending data to the buffer --------------------------------//
+
+    @Test
+    public void testAppendToBufferAtEndOfContentArray() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] source1 = new byte[] { 0, 1, 2, 3 };
+
+        assertTrue(buffer.hasArray());
+        assertEquals(0, buffer.numberOfBuffers());
+
+        buffer.append(source1);
+
+        assertTrue(buffer.hasArray());
+        assertEquals(1, buffer.numberOfBuffers());
+
+        buffer.setReadIndex(source1.length);
+
+        assertFalse(buffer.isReadable());
+        assertEquals(0, buffer.getReadableBytes());
+
+        byte[] source2 = new byte[] { 4, 5, 6, 7 };
+        buffer.append(source2);
+
+        assertTrue(buffer.isReadable());
+        assertEquals(source2.length, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertEquals(2, buffer.numberOfBuffers());
+        assertEquals(source1.length, buffer.getReadIndex());
+
+        // Check each position in the array is read
+        for(int i = 0; i < source2.length; i++) {
+            assertEquals(source1.length + i, buffer.readByte());
+        }
+    }
+
+    @Test
+    public void testAppendToBufferAtEndOfContentList() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] source1 = new byte[] { 0, 1, 2, 3 };
+        byte[] source2 = new byte[] { 4, 5, 6, 7 };
+
+        buffer.append(source1);
+        buffer.append(source2);
+
+        assertFalse(buffer.hasArray());
+        assertEquals(2, buffer.numberOfBuffers());
+
+        buffer.setReadIndex(source1.length + source2.length);
+
+        assertFalse(buffer.isReadable());
+        assertEquals(0, buffer.getReadableBytes());
+
+        byte[] source3 = new byte[] { 8, 9, 10, 11 };
+        buffer.append(source3);
+
+        assertTrue(buffer.isReadable());
+        assertEquals(source3.length, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertEquals(3, buffer.numberOfBuffers());
+        assertEquals(source1.length + source2.length, buffer.getReadIndex());
+
+        // Check each position in the array is read
+        for(int i = 0; i < source3.length; i++) {
+            assertEquals(source1.length + source2.length + i, buffer.readByte());
+        }
+    }
+
+    @Test
+    public void testAppendToBufferAtWhenWriteIndexNotAtEnd() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        byte[] source1 = new byte[] { 0, 1, 2, 3 };
+        byte[] source2 = new byte[] { 4, 5, 6, 7 };
+
+        buffer.append(source1);
+
+        assertEquals(source1.length, buffer.getWriteIndex());
+
+        buffer.append(source2);
+
+        assertEquals(source2.length + source1.length, buffer.getWriteIndex());
+
+        byte[] source3 = new byte[] { 8, 9, 10, 11 };
+
+        buffer.setWriteIndex(2);
+
+        buffer.append(source3);
+
+        assertEquals(2, buffer.getWriteIndex());
+        assertFalse(buffer.hasArray());
+        assertEquals(3, buffer.numberOfBuffers());
+    }
+
+    @Test
+    public void testAppendNullByteArray() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        try {
+            buffer.append((byte[]) null);
+            fail("Should not be able to add a null array");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testAppendNullByteArrayWithArgs() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        try {
+            buffer.append((byte[]) null, 0, 0);
+            fail("Should not be able to add a null array");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testAppendNullReadableBuffer() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        try {
+            buffer.append((ProtonBuffer) null);
+            fail("Should not be able to add a null array");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testAppendEmptyByteArray() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[0]);
+
+        assertFalse(buffer.isReadable());
+        assertTrue(buffer.hasArray());
+        assertEquals(0, buffer.numberOfBuffers());
+    }
+
+    //----- Test various cases of Duplicate ----------------------------------//
+
+    @Test
+    public void testDuplicateOnEmptyBuffer() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        ProtonBuffer dup = buffer.duplicate();
+
+        assertNotSame(buffer, dup);
+        assertEquals(0, dup.capacity());
+        assertEquals(0, buffer.capacity());
+        assertEquals(0, dup.getReadIndex());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(0, dup.getWriteIndex());
+        assertEquals(0, buffer.getWriteIndex());
+        assertContentEquals(buffer, dup);
+    }
+
+    @Test
+    public void testDuplicateWithSingleArrayContent() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+        buffer.markReadIndex();
+        buffer.setReadIndex(buffer.getWriteIndex());
+
+        // duplicate's contents should be the same as buffer
+        ProtonBuffer duplicate = buffer.duplicate();
+        assertNotSame(buffer, duplicate);
+        assertEquals(buffer.capacity(), duplicate.capacity());
+        assertEquals(buffer.getReadIndex(), duplicate.getReadIndex());
+        assertEquals(buffer.getWriteIndex(), duplicate.getWriteIndex());
+        assertContentEquals(buffer, duplicate);
+
+        // duplicate's read index, mark, and write index should be independent to buffer
+        duplicate.resetReadIndex();
+        assertEquals(duplicate.getReadIndex(), duplicate.getWriteIndex());
+        duplicate.clear();
+        assertEquals(buffer.getReadIndex(), buffer.getWriteIndex());
+        buffer.resetReadIndex();
+        assertEquals(buffer.getReadIndex(), 0);
+
+        // One array buffer should share backing array
+        assertTrue(buffer.hasArray());
+        assertTrue(duplicate.hasArray());
+        assertSame(buffer.getArray(), duplicate.getArray());
+    }
+
+    @Test
+    public void testDuplicateWithSingleArrayContentCompactionIsNoOpWhenNotRead() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+
+        ProtonBuffer duplicate = buffer.duplicate();
+
+        assertEquals(10, buffer.capacity());
+        assertEquals(buffer.capacity(), duplicate.capacity());
+    }
+
+    @Test
+    public void testAppendedBufferCannotForceMaxCapacityExceeded() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer(6);
+
+        byte[] source1 = new byte[] { 0, 1, 2, 3 };
+        byte[] source2 = new byte[] { 4, 5, 6, 7 };
+
+        buffer.append(source1);
+        assertEquals(source1.length, buffer.capacity());
+
+        try {
+            buffer.append(source2);
+            fail("Should not be able to exceed max capacity limit.");
+        } catch (IndexOutOfBoundsException iae) {
+        }
+
+        assertEquals(source1.length, buffer.capacity());
+    }
+
+    //----- Tests for hashCode -----------------------------------------------//
+
+    @Test
+    public void testHashCodeNotFromIdentity() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        assertEquals(1, buffer.hashCode());
+
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        buffer.append(data);
+
+        assertTrue(buffer.hashCode() != 1);
+        assertNotEquals(buffer.hashCode(), System.identityHashCode(buffer));
+        assertEquals(buffer.hashCode(), buffer.hashCode());
+    }
+
+    @Test
+    public void testHashCodeOnSameBackingBuffer() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer3 = new ProtonCompositeBuffer();
+
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        buffer1.append(data);
+        buffer2.append(data);
+        buffer3.append(data);
+
+        assertEquals(buffer1.hashCode(), buffer2.hashCode());
+        assertEquals(buffer2.hashCode(), buffer3.hashCode());
+        assertEquals(buffer3.hashCode(), buffer1.hashCode());
+    }
+
+    @Test
+    public void testHashCodeOnDifferentBackingBuffer() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+        byte[] data2 = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer1.append(data1);
+        buffer2.append(data2);
+
+        assertNotEquals(buffer1.hashCode(), buffer2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeOnSplitBufferContentsNotSame() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+        byte[] data2 = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer1.append(data1).append(data2);
+        buffer2.append(data2).append(data1);
+
+        assertNotEquals(buffer1.hashCode(), buffer2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeOnSplitBufferContentsSame() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+        byte[] data2 = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer1.append(data1).append(data2);
+        buffer2.append(data1).append(data2);
+
+        assertEquals(buffer1.hashCode(), buffer2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeMatchesByteBufferWhenLimitSetGivesNoRemaining() throws CharacterCodingException {
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        buffer1.append(data);
+        buffer1.setReadIndex(buffer1.getWriteIndex());
+
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(data);
+        buffer2.setReadIndex(buffer1.getWriteIndex());
+
+        assertEquals(buffer1.hashCode(), buffer2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeMatchesByteBufferSingleArrayContents() throws CharacterCodingException {
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        buffer1.append(data);
+
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(data);
+
+        assertEquals(buffer1.hashCode(), buffer2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeMatchesByteBufferSingleArrayContentsWithSlice() throws CharacterCodingException {
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        buffer1.append(data);
+
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(data);
+
+        ProtonBuffer slice1 = buffer1.setReadIndex(1).slice();
+        ProtonBuffer slice2 = buffer2.setReadIndex(1).slice();
+
+        assertEquals(slice1.hashCode(), slice2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeMatchesByteBufferMultipleArrayContents() throws CharacterCodingException {
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5};
+        byte[] data2 = new byte[] {4, 3, 2, 1, 0};
+
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        buffer1.append(data1);
+        buffer1.append(data2);
+
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(data);
+
+        assertEquals(buffer1.hashCode(), buffer2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeMatchesByteBufferMultipleArrayContentsWithSlice() throws CharacterCodingException {
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5};
+        byte[] data2 = new byte[] {4, 3, 2, 1, 0};
+
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        buffer1.append(data1);
+        buffer1.append(data2);
+
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(data);
+
+        ProtonBuffer slice1 = buffer1.setReadIndex(1).setWriteIndex(4).slice();
+        ProtonBuffer slice2 = buffer2.setReadIndex(1).setWriteIndex(4).slice();
+
+        assertEquals(slice1.hashCode(), slice2.hashCode());
+    }
+
+    @Test
+    public void testHashCodeMatchesByteBufferMultipleArrayContentsWithRangeOfLimits() throws CharacterCodingException {
+        byte[] data = new byte[] {10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        byte[] data1 = new byte[] {10, 9};
+        byte[] data2 = new byte[] {8, 7};
+        byte[] data3 = new byte[] {6, 5, 4};
+        byte[] data4 = new byte[] {3};
+        byte[] data5 = new byte[] {2, 1, 0};
+
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        buffer1.append(data1).append(data2).append(data3).append(data4).append(data5);
+
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(data);
+
+        for (int i = 0; i < data.length; ++i) {
+            buffer1.setWriteIndex(i);
+            buffer2.setWriteIndex(i);
+
+            assertEquals(buffer1.hashCode(), buffer2.hashCode());
+        }
+    }
+
+    //----- Tests for equals -------------------------------------------------//
+
+    @Test
+    public void testEqualsOnSameBackingBuffer() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer3 = new ProtonCompositeBuffer();
+
+        byte[] data = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+        buffer1.append(data);
+        buffer2.append(data);
+        buffer3.append(data);
+
+        assertEquals(buffer1, buffer2);
+        assertEquals(buffer2, buffer3);
+        assertEquals(buffer3, buffer1);
+
+        assertEquals(0, buffer1.getReadIndex());
+        assertEquals(0, buffer2.getReadIndex());
+        assertEquals(0, buffer3.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsOnDifferentBackingBuffer() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+        byte[] data2 = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer1.append(data1);
+        buffer2.append(data2);
+
+        assertNotEquals(buffer1, buffer2);
+
+        assertEquals(0, buffer1.getReadIndex());
+        assertEquals(0, buffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentsInMultipleArraysNotSame() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+        byte[] data2 = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer1.append(data1).append(data2);
+        buffer2.append(data2).append(data1);
+
+        assertNotEquals(buffer1, buffer2);
+
+        assertEquals(0, buffer1.getReadIndex());
+        assertEquals(0, buffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentsInMultipleArraysSame() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+        byte[] data2 = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        buffer1.append(data1).append(data2);
+        buffer2.append(data1).append(data2);
+
+        assertEquals(buffer1, buffer2);
+
+        assertEquals(0, buffer1.getReadIndex());
+        assertEquals(0, buffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentStartPositionsSame() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentStartPositionsSameTestImpl(false);
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentStartPositionsSameMultipleArrays() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentStartPositionsSameTestImpl(true);
+    }
+
+    private void doEqualsWhenContentRemainingWithDifferentStartPositionsSameTestImpl(boolean multipleArrays) {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {-1, -1, 0, 1, 2, 3, 4, 5};
+        byte[] data2 = new byte[] {-1, -1, -1, 0, 1, 2, 3, 4, 5};
+
+        buffer1.append(data1);
+        buffer1.setReadIndex(2);
+        buffer1.markWriteIndex();
+
+        // Offset wrapped buffer should behave same as buffer 1
+        buffer2.append(data2, 1, data1.length);
+        buffer2.setReadIndex(2);
+        buffer2.markWriteIndex();
+
+        if (multipleArrays) {
+            byte[] data3 = new byte[] { 5, 4, 3, 2, 1 };
+            buffer1.append(data3).resetWriteIndex();
+            buffer2.append(data3).resetWriteIndex();
+        }
+
+        assertEquals(buffer1, buffer2);
+
+        assertEquals(2, buffer1.getReadIndex());
+        assertEquals(2, buffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentStartPositionsNotSame() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentStartPositionsNotSameTestImpl(false);
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentStartPositionsNotSameMultipleArrays() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentStartPositionsNotSameTestImpl(true);
+    }
+
+    private void doEqualsWhenContentRemainingWithDifferentStartPositionsNotSameTestImpl(boolean multipleArrays) {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {-1, -1, 0, 1, 2, 3, 4, 5};
+        byte[] data2 = new byte[] {-1, -1, -1, 0, 1, 2, 3, 4, -1};
+
+        buffer1.append(data1);
+        buffer1.setReadIndex(2);
+
+        buffer2.append(data2);
+        buffer2.setReadIndex(3);
+
+        if (multipleArrays) {
+            byte[] data3 = new byte[] { 5, 4, 3, 2, 1 };
+            buffer1.append(data3);
+            buffer2.append(data3);
+        }
+
+        assertNotEquals(buffer1, buffer2);
+
+        assertEquals(2, buffer1.getReadIndex());
+        assertEquals(3, buffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentlyPositionedSlicesSame() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentlyPositionedSlicesSameTestImpl(false);
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentlyPositionedSlicesSameMultipleArrays() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentlyPositionedSlicesSameTestImpl(true);
+    }
+
+    private void doEqualsWhenContentRemainingWithDifferentlyPositionedSlicesSameTestImpl(boolean multipleArrays) {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {-1, -1, 0, 1, 2, 3, 4, 5};
+        byte[] data2 = new byte[] {-1, -1, -1, 0, 1, 2, 3, 4, 5};
+
+        buffer1.append(data1);
+        buffer1.setReadIndex(2);
+
+        buffer2.append(data2);
+        buffer2.setReadIndex(3);
+
+        if (multipleArrays) {
+            byte[] data3 = new byte[] { 5, 4, 3, 2, 1 };
+            buffer1.append(data3);
+            buffer2.append(data3);
+        }
+
+        ProtonBuffer slicedBuffer1 = buffer1.slice();
+        ProtonBuffer slicedBuffer2 = buffer2.slice();
+
+        assertEquals(slicedBuffer1, slicedBuffer2);
+        assertEquals(slicedBuffer2, slicedBuffer1);
+
+        assertEquals(0, slicedBuffer1.getReadIndex());
+        assertEquals(0, slicedBuffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentlyPositionedSlicesNotSame() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentlyPositionedSlicesNotSameTestImpl(false);
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingWithDifferentlyPositionedSlicesNotSameMultipleArrays() throws CharacterCodingException {
+        doEqualsWhenContentRemainingWithDifferentlyPositionedSlicesNotSameTestImpl(true);
+    }
+
+    private void doEqualsWhenContentRemainingWithDifferentlyPositionedSlicesNotSameTestImpl(boolean multipleArrays) {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {-1, -1, 0, 1, 2, 3, 4, 5};
+        byte[] data2 = new byte[] {-1, -1, -1, 0, 1, 2, 3, 4, -1};
+
+        buffer1.append(data1);
+        buffer1.setReadIndex(2);
+
+        buffer2.append(data2);
+        buffer2.setReadIndex(3);
+
+        if (multipleArrays) {
+            byte[] data3 = new byte[] { 5, 4, 3, 2, 1 };
+            buffer1.append(data3);
+            buffer2.append(data3);
+        }
+
+        ProtonBuffer slicedBuffer1 = buffer1.slice();
+        ProtonBuffer slicedBuffer2 = buffer2.slice();
+
+        assertNotEquals(slicedBuffer1, slicedBuffer2);
+        assertNotEquals(slicedBuffer2, slicedBuffer1);
+
+        assertEquals(0, slicedBuffer1.getReadIndex());
+        assertEquals(0, slicedBuffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingIsSubsetOfSingleChunkInMultiArrayBufferSame() {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {-1, -1, 0, 1, 2, 3, 4, 5};
+        byte[] data2 = new byte[] {-1, -1, -1, 0, 1, 2, 3, 4, 5};
+
+        buffer1.append(data1);
+        buffer1.setReadIndex(2);
+
+        // Offset the wrapped buffer which means these two should behave the same
+        buffer2.append(data2, 1, data2.length - 1);
+        buffer2.setReadIndex(2);
+
+        byte[] data3 = new byte[] { 5, 4, 3, 2, 1 };
+        buffer1.append(data3);
+        buffer2.append(data3);
+
+        buffer1.setWriteIndex(data1.length);
+        buffer2.setWriteIndex(data1.length);
+
+        assertEquals(6, buffer1.getReadableBytes());
+        assertEquals(6, buffer2.getReadableBytes());
+
+        assertEquals(buffer1, buffer2);
+        assertEquals(buffer2, buffer1);
+
+        assertEquals(2, buffer1.getReadIndex());
+        assertEquals(2, buffer2.getReadIndex());
+    }
+
+    @Test
+    public void testEqualsWhenContentRemainingIsSubsetOfSingleChunkInMultiArrayBufferNotSame() {
+        ProtonCompositeBuffer buffer1 = new ProtonCompositeBuffer();
+        ProtonCompositeBuffer buffer2 = new ProtonCompositeBuffer();
+
+        byte[] data1 = new byte[] {-1, -1, 0, 1, 2, 3, 4, 5};
+        byte[] data2 = new byte[] {-1, -1, -1, 0, 1, 2, 3, 4, -1};
+
+        buffer1.append(data1);
+        buffer1.setReadIndex(2);
+
+        buffer2.append(data2);
+        buffer2.setReadIndex(3);
+
+        byte[] data3 = new byte[] { 5, 4, 3, 2, 1 };
+        buffer1.append(data3);
+        buffer2.append(data3);
+
+        buffer1.setWriteIndex(data1.length);
+        buffer2.setWriteIndex(data2.length);
+
+        assertEquals(6, buffer1.getReadableBytes());
+        assertEquals(6, buffer2.getReadableBytes());
+
+        assertNotEquals(buffer1, buffer2);
+        assertNotEquals(buffer2, buffer1);
+
+        assertEquals(2, buffer1.getReadIndex());
+        assertEquals(3, buffer2.getReadIndex());
+    }
+
+    //----- Test toByteBuffer implementation for Composites
+
+    @Test
+    public void testToByteBufferWhenEmpty() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        assertNotNull(buffer.toByteBuffer());
+        assertSame(buffer.toByteBuffer(), buffer.toByteBuffer());
+        assertEquals(0, buffer.toByteBuffer().capacity());
+    }
+
+    @Test
+    public void testToByteBufferAcrossArrays() {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        buffer.append(new byte[] {0})
+              .append(new byte[] {1, 2})
+              .append(new byte[] {3, 4, 5})
+              .append(new byte[] {6})
+              .append(new byte[] {7, 8, 9});
+
+        assertEquals(10, buffer.getReadableBytes());
+        assertFalse(buffer.hasArray());
+        assertTrue(buffer.isReadable());
+        assertEquals(0, buffer.getReadIndex());
+        assertEquals(10, buffer.getWriteIndex());
+
+        ByteBuffer nioBuffer = buffer.toByteBuffer(0, 1);
+        assertNotNull(nioBuffer);
+        assertEquals(1,  nioBuffer.capacity());
+        assertEquals(0, nioBuffer.get(0));
+
+        nioBuffer = buffer.toByteBuffer(5, 5);
+        assertNotNull(nioBuffer);
+        assertEquals(5,  nioBuffer.capacity());
+        assertEquals(5, nioBuffer.get(0));
+        assertEquals(6, nioBuffer.get(1));
+        assertEquals(7, nioBuffer.get(2));
+        assertEquals(8, nioBuffer.get(3));
+        assertEquals(9, nioBuffer.get(4));
+    }
+
+    //----- Tests for altering capacity of composite buffer instances
+
+    @Test
+    public void testReduceCapacityAndReadSequentialShortValues() throws CharacterCodingException {
+        byte[] data1 = new byte[] {0, 1, 0, 2, 0, 3, 0, 4};
+        byte[] data2 = new byte[] {0, 5, 0, 6, 0, 7, 0, 8};
+        byte[] data3 = new byte[] {0, 9, 0, 10, 0, 11, 0, 12};
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(data1).append(data2).append(data3);
+
+        final int initialNumShorts = buffer.capacity() / 2;
+
+        for (int i = 0; i < initialNumShorts; ++i) {
+            assertEquals(i + 1, buffer.readShort());
+        }
+
+        buffer.setReadIndex(0);
+        buffer.capacity(buffer.capacity() / 2);
+
+        final int newNumShorts = buffer.capacity() / 2;
+        assertEquals(initialNumShorts / 2, newNumShorts);
+
+        for (int i = 0; i < newNumShorts; ++i) {
+            assertEquals(i + 1, buffer.readShort());
+        }
+    }
+
+    @Test
+    public void testReduceCapacityToZero() throws CharacterCodingException {
+        byte[] data1 = new byte[] {0, 1, 0, 2, 0, 3, 0, 4};
+        byte[] data2 = new byte[] {0, 5, 0, 6, 0, 7, 0, 8};
+        byte[] data3 = new byte[] {0, 9, 0, 10, 0, 11, 0, 12, 0, 13};
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(data1).append(data2);
+
+        assertFalse(buffer.hasArray());
+        assertEquals(data1.length + data2.length, buffer.capacity());
+
+        buffer.capacity(0);
+
+        buffer.append(data3);
+
+        assertEquals(data3.length, buffer.capacity());
+        assertTrue(buffer.hasArray());
+    }
+
+    //----- Test Access to composite buffers when they are offset
+
+    @Test
+    public void testCompositeWithOffsetBuffersReadsSequentialShorts() throws CharacterCodingException {
+        byte[] data1 = new byte[] {1, 1, 0, 0, 0, 1, 0, 2};
+        byte[] data2 = new byte[] {0, 3, 0, 4, 0, 5, 1, 1};
+        byte[] data3 = new byte[] {1, 1, 1, 1, 0, 6, 0, 7};
+
+        ProtonBuffer offset1 = new ProtonByteBuffer(data1).skipBytes(2);
+        ProtonBuffer offset2 = new ProtonByteBuffer(data2).setWriteIndex(data2.length - 2);
+        ProtonBuffer offset3 = new ProtonByteBuffer(data3).skipBytes(4);
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(offset1).append(offset2).append(offset3);
+
+        assertEquals(data1.length + data2.length + data3.length, buffer.capacity() + 8);
+
+        final int initialNumShorts = buffer.capacity() / 2;
+
+        for (int i = 0; i < initialNumShorts; ++i) {
+            assertEquals(i, buffer.readShort());
+        }
+    }
+
+    @Test
+    public void testByteArrayTransferWithOffsetComposites() {
+        testByteArrayTransfer(false);
+    }
+
+    @Test
+    public void testByteArrayTransferDirectBackedBufferOfOffsetComposites() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        testByteArrayTransfer(true);
+    }
+
+    private void testByteArrayTransfer(boolean direct) {
+        final ProtonBuffer buffer;
+
+        if (direct) {
+            buffer = allocateDirectBufferOfOffsetComposites(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBufferOfOffsetComposites(LARGE_CAPACITY);
+        }
+
+        byte[] value = new byte[BLOCK_SIZE * 2];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(value);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValue = new byte[BLOCK_SIZE * 2];
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValue);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue[j], value[j]);
+            }
+        }
+    }
+
+    //----- Test buffer walking for each methods
+
+    @Test
+    public void testForeachBufferReturnsDuplicates() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        assertEquals(2, composite.numberOfBuffers());
+
+        final AtomicInteger walked = new AtomicInteger();
+
+        composite.foreachBuffer(buffer -> {
+            walked.incrementAndGet();
+
+            if (buffer == buffer1 || buffer == buffer2) {
+                throw new AssertionError("Buffer returned should not be any of the source buffers.");
+            }
+        });
+
+        assertEquals(2, walked.get());
+    }
+
+    @Test
+    public void testForeachInternalBufferReturnsDuplicates() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        assertEquals(2, composite.numberOfBuffers());
+
+        final AtomicInteger walked = new AtomicInteger();
+
+        composite.foreachInternalBuffer(buffer -> {
+            walked.incrementAndGet();
+
+            if (buffer != buffer1 && buffer != buffer2) {
+                throw new AssertionError("Buffer returned should be one of the source buffers.");
+            }
+        });
+
+        assertEquals(2, walked.get());
+    }
+
+    //----- Test reclaim read buffers to preserve memory
+
+    @Test
+    public void testReclaimBuffersFromEmptyComposite() {
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        assertEquals(0, composite.getReadableBytes());
+        assertEquals(0, composite.numberOfBuffers());
+
+        composite.reclaimRead();
+
+        assertEquals(0, composite.numberOfBuffers());
+        assertEquals(0, composite.getReadableBytes());
+    }
+
+    @Test
+    public void testReclaimBufferWhenNothingReadHasNoEffect() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+
+        assertEquals(buffer1.getReadableBytes(), composite.getReadableBytes());
+        assertEquals(1, composite.numberOfBuffers());
+
+        composite.reclaimRead();
+
+        assertEquals(buffer1.getReadableBytes(), composite.getReadableBytes());
+        assertEquals(1, composite.numberOfBuffers());
+    }
+
+    @Test
+    public void testReclaimAfterSingleBufferFullyRead() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+
+        assertEquals(buffer1.getReadableBytes(), composite.getReadableBytes());
+
+        composite.setReadIndex(buffer1.getReadableBytes());
+
+        assertEquals(buffer1.getReadableBytes(), composite.getReadIndex());
+        assertEquals(buffer1.getReadableBytes(), composite.getWriteIndex());
+        assertEquals(1, composite.numberOfBuffers());
+        assertEquals(0, composite.getReadableBytes());
+        assertEquals(0, composite.getWritableBytes());
+
+        composite.reclaimRead();
+
+        assertEquals(0, composite.getReadIndex());
+        assertEquals(0, composite.getWriteIndex());
+        assertEquals(0, composite.capacity());
+        assertEquals(0, composite.getReadableBytes());
+        assertEquals(0, composite.numberOfBuffers());
+    }
+
+    @Test
+    public void testReclaimAfterSingleBufferReadToOnyByteLeft() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+
+        assertEquals(buffer1.getReadableBytes(), composite.getReadableBytes());
+
+        composite.setReadIndex(buffer1.getReadableBytes() - 1);
+
+        assertEquals(1, composite.getReadableBytes());
+        assertEquals(1, composite.numberOfBuffers());
+
+        composite.reclaimRead();
+
+        assertEquals(1, composite.getReadableBytes());
+        assertEquals(1, composite.numberOfBuffers());
+
+        composite.readByte();
+        composite.reclaimRead();
+
+        assertEquals(0, composite.getReadableBytes());
+        assertEquals(0, composite.numberOfBuffers());
+    }
+
+    @Test
+    public void testReclaimReadBuffersWhenNoneRead() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        assertEquals(2, composite.numberOfBuffers());
+        assertEquals(buffer1.getReadableBytes() + buffer2.getReadableBytes(), composite.getReadableBytes());
+
+        composite.reclaimRead();
+
+        assertEquals(2, composite.numberOfBuffers());
+        assertEquals(buffer1.getReadableBytes() + buffer2.getReadableBytes(), composite.getReadableBytes());
+    }
+
+    @Test
+    public void testReclaimAllBuffers() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        assertEquals(2, composite.numberOfBuffers());
+        assertEquals(buffer1.getReadableBytes() + buffer2.getReadableBytes(), composite.getReadableBytes());
+
+        composite.setIndex(composite.getReadableBytes(), composite.getReadableBytes());
+
+        assertEquals(0, composite.getReadableBytes());
+
+        composite.reclaimRead();
+
+        assertEquals(0, composite.numberOfBuffers());
+        assertEquals(0, composite.getReadableBytes());
+    }
+
+    @Test
+    public void testReclaimFirstReadChunk() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0  });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        assertEquals(2, composite.numberOfBuffers());
+        assertEquals(buffer1.getReadableBytes() + buffer2.getReadableBytes(), composite.getReadableBytes());
+
+        composite.setIndex(buffer1.getReadableBytes(), composite.getReadableBytes());
+
+        assertEquals(buffer2.getReadableBytes(), composite.getReadableBytes());
+
+        composite.reclaimRead();
+
+        assertEquals(1, composite.numberOfBuffers());
+        assertEquals(buffer2.getReadableBytes(), composite.getReadableBytes());
+
+        for (int i = 0; i < buffer2.getReadableBytes(); ++i) {
+            assertEquals(buffer2.getByte(i), composite.readByte());
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> composite.readByte());
+    }
+
+    @Test
+    public void testReclaimFirstReadChunksWithMultplePendingBuffers() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0  });
+        ProtonBuffer buffer3 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer4 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0  });
+        ProtonBuffer buffer5 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer6 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0  });
+
+        final int totalPayload = buffer1.capacity() * 6;
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+        composite.append(buffer3);
+        composite.append(buffer4);
+        composite.append(buffer5);
+        composite.append(buffer6);
+
+        assertEquals(6, composite.numberOfBuffers());
+        assertEquals(totalPayload, composite.getReadableBytes());
+
+        composite.setIndex(buffer1.getReadableBytes() * 2, composite.getReadableBytes());
+
+        assertEquals(buffer1.capacity() * 4, composite.getReadableBytes());
+
+        composite.reclaimRead();
+
+        assertEquals(4, composite.numberOfBuffers());
+        assertEquals(buffer1.capacity() * 4, composite.getReadableBytes());
+
+        for (int i = 0; i < buffer3.getReadableBytes(); ++i) {
+            assertEquals(buffer3.getByte(i), composite.readByte());
+        }
+        for (int i = 0; i < buffer4.getReadableBytes(); ++i) {
+            assertEquals(buffer4.getByte(i), composite.readByte());
+        }
+        for (int i = 0; i < buffer5.getReadableBytes(); ++i) {
+            assertEquals(buffer5.getByte(i), composite.readByte());
+        }
+        for (int i = 0; i < buffer6.getReadableBytes(); ++i) {
+            assertEquals(buffer6.getByte(i), composite.readByte());
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> composite.readByte());
+    }
+
+    @Test
+    public void testReclaimBufferWhenIndexIsBeyondStartOfNextBuffer() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0  });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        assertEquals(2, composite.numberOfBuffers());
+        assertEquals(buffer1.getReadableBytes() + buffer2.getReadableBytes(), composite.getReadableBytes());
+
+        composite.setIndex(buffer1.getReadableBytes() + 2, composite.getReadableBytes());
+
+        assertEquals(buffer2.getReadableBytes() - 2, composite.getReadableBytes());
+
+        composite.reclaimRead();
+
+        assertEquals(1, composite.numberOfBuffers());
+        assertEquals(buffer2.getReadableBytes() - 2, composite.getReadableBytes());
+
+        for (int i = 2; i < buffer2.getReadableBytes(); ++i) {
+            assertEquals(buffer2.getByte(i), composite.readByte());
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> composite.readByte());
+    }
+
+    @Test
+    public void testReclaimBufferWhileMarksSetInSuccessiveBuffer() {
+        ProtonBuffer buffer1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+        ProtonBuffer buffer2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0  });
+
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer();
+
+        composite.append(buffer1);
+        composite.append(buffer2);
+
+        composite.setIndex(buffer1.getReadableBytes(), composite.getReadableBytes());
+
+        assertEquals(buffer2.getByte(0), composite.readByte());
+
+        composite.markReadIndex();
+
+        assertEquals(buffer2.getByte(1), composite.readByte());
+        assertEquals(buffer2.getByte(2), composite.readByte());
+
+        composite.reclaimRead();
+        composite.resetReadIndex();
+
+        assertEquals(buffer2.getByte(1), composite.readByte());
+        assertEquals(buffer2.getByte(2), composite.readByte());
+    }
+
+    //----- Test readString ------------------------------------------//
+
+    @Test
+    public void testReadStringFromEmptyBuffer() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        assertEquals("", buffer.toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testReadStringFromUTF8InSingleArray() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final String testString = "Test String to Decode!";
+        byte[] encoded = testString.getBytes(StandardCharsets.UTF_8);
+
+        buffer.append(encoded);
+
+        assertEquals(testString, buffer.toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testReadStringFromUTF8InSingleArrayWithLimits() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final String testString = "Test String to Decode!";
+        byte[] encoded = testString.getBytes(StandardCharsets.UTF_8);
+
+        // Only read the first character
+        buffer.append(encoded);
+        buffer.setWriteIndex(1);
+
+        assertEquals("T", buffer.toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testReadStringFromUTF8InMulitpleArrays() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final String testString = "Test String to Decode!!";
+        byte[] encoded = testString.getBytes(StandardCharsets.UTF_8);
+
+        byte[] first = new byte[encoded.length / 2];
+        byte[] second = new byte[encoded.length - (encoded.length / 2)];
+
+        System.arraycopy(encoded, 0, first, 0, first.length);
+        System.arraycopy(encoded, first.length, second, 0, second.length);
+
+        buffer.append(first).append(second);
+
+        String result = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(testString, result);
+    }
+
+    @Test
+    public void testReadStringFromUTF8InMultipleArraysWithLimits() throws CharacterCodingException {
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+
+        final String testString = "Test String to Decode!";
+        byte[] encoded = testString.getBytes(StandardCharsets.UTF_8);
+
+        byte[] first = new byte[encoded.length / 2];
+        byte[] second = new byte[encoded.length - (encoded.length / 2)];
+
+        System.arraycopy(encoded, 0, first, 0, first.length);
+        System.arraycopy(encoded, first.length, second, 0, second.length);
+
+        buffer.append(first).append(second);
+
+        // Only read the first character
+        buffer.setWriteIndex(1);
+
+        assertEquals("T", buffer.toString(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    @Test
+    public void testReadUnicodeStringAcrossArrayBoundries() throws IOException {
+        String expected = "\u1f4a9\u1f4a9\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[utf8.length - 1];
+
+        System.arraycopy(utf8, 1, slice2, 0, slice2.length);
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(slice1);
+        buffer.append(slice2);
+
+        String result = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Override
+    @Test
+    public void testReadUnicodeStringAcrossMultipleArrayBoundries() throws IOException {
+        String expected = "\u1f4a9\u1f4a9\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[] { utf8[1], utf8[2] };
+        byte[] slice3 = new byte[] { utf8[3], utf8[4] };
+        byte[] slice4 = new byte[utf8.length - 5];
+
+        System.arraycopy(utf8, 5, slice4, 0, slice4.length);
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(slice1);
+        buffer.append(slice2);
+        buffer.append(slice3);
+        buffer.append(slice4);
+
+        String result = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Test
+    public void testReadUnicodeStringEachByteInOwnArray() throws IOException {
+        String expected = "\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        assertEquals(4, utf8.length);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[] { utf8[1] };
+        byte[] slice3 = new byte[] { utf8[2] };
+        byte[] slice4 = new byte[] { utf8[3] };
+
+        System.arraycopy(utf8, 1, slice2, 0, slice2.length);
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(slice1);
+        buffer.append(slice2);
+        buffer.append(slice3);
+        buffer.append(slice4);
+
+        String result = buffer.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Test
+    public void testReadSlicedWithInvalidEncodingsOutsideSlicedRange() throws IOException {
+        String expected = "\u1f4a9\u1f4a9\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] payload = new byte[utf8.length + 2];  // Add two for malformed UTF8
+
+        System.arraycopy(utf8, 0, payload, 0, utf8.length);
+
+        payload[utf8.length] = (byte) 0b11000111;     // Two byte utf8 encoding prefix
+        payload[utf8.length + 1] = (byte) 0b00110000; // invalid next byte encoding should be 0b10xxxxxx
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer().append(payload);
+        ProtonBuffer slicedComposite = buffer.setWriteIndex(utf8.length).slice();
+
+        String result = slicedComposite.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Test
+    public void testReadSliceWithInvalidEncodingsOutsideSlicedRangeWithArraySpans() throws IOException {
+        String expected = "\u1f4a9\u1f4a9\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] span1 = new byte[] { utf8[0] };
+        byte[] span2 = new byte[utf8.length + 2];  // Add two for malformed UTF8
+
+        System.arraycopy(utf8, 1, span2, 0, utf8.length - 1);
+
+        span2[utf8.length] = (byte) 0b11000111;     // Two byte utf8 encoding prefix
+        span2[utf8.length + 1] = (byte) 0b00110000; // invalid next byte encoding should be 0b10xxxxxx
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(span1);
+        buffer.append(span2);
+
+        ProtonBuffer slicedComposite = buffer.setWriteIndex(utf8.length).slice();
+
+        String result = slicedComposite.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Test
+    public void testReadSliceWithInvalidEncodingsOutsideSlicedRangeWithArraySpansAndEarlySpan() throws IOException {
+        String expected = "\u1f4a9\u1f4a9\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] span1 = new byte[] { 0, 1, 2, 3, 4 };
+        byte[] span2 = new byte[] { utf8[0] };
+        byte[] span3 = new byte[utf8.length + 2];  // Add two for malformed UTF8
+
+        System.arraycopy(utf8, 1, span3, 0, utf8.length - 1);
+
+        span3[utf8.length] = (byte) 0b11000111;     // Two byte utf8 encoding prefix
+        span3[utf8.length + 1] = (byte) 0b00110000; // invalid next byte encoding should be 0b10xxxxxx
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(span1);
+        buffer.append(span2);
+        buffer.append(span3);
+
+        ProtonBuffer slicedComposite = buffer.setReadIndex(span1.length).setWriteIndex(span1.length + utf8.length).slice();
+
+        String result = slicedComposite.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    @Test
+    public void testReadSliceWithInvalidEncodingsSurroundingSlicedSpanningRanges() throws IOException {
+        String expected = "\u1f4a9\u1f4a9\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] span1 = new byte[] { (byte) 0b11000111, 0b00110000, utf8[0] };
+        byte[] span2 = new byte[] { utf8[1] };
+        byte[] span3 = new byte[utf8.length];  // provides two slots for malformed UTF8
+
+        System.arraycopy(utf8, 2, span3, 0, utf8.length - 2);
+
+        span3[span3.length - 2] = (byte) 0b11000111;     // Two byte utf8 encoding prefix
+        span3[span3.length - 1] = (byte) 0b00110000; // invalid next byte encoding should be 0b10xxxxxx
+
+        ProtonCompositeBuffer buffer = new ProtonCompositeBuffer();
+        buffer.append(span1);
+        buffer.append(span2);
+        buffer.append(span3);
+
+        // Start at first utf8 byte and run to end of span 2 minus the trailing
+        ProtonBuffer slicedComposite = buffer.setIndex(span1.length - 1, span1.length + utf8.length - 1).slice();
+
+        String result = slicedComposite.toString(StandardCharsets.UTF_8);
+
+        assertEquals(expected, result, "Failed to round trip String correctly: ");
+    }
+
+    //----- Implement abstract methods from the abstract buffer test base class
+
+    @Override
+    protected boolean canAllocateDirectBackedBuffers() {
+        return true;
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity) {
+        return new ProtonCompositeBuffer(Integer.MAX_VALUE).capacity(initialCapacity);
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity) {
+        return new ProtonCompositeBuffer(Integer.MAX_VALUE).append(
+            new ProtonNioByteBuffer(ByteBuffer.allocateDirect(initialCapacity), 0));
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonCompositeBuffer(maxCapacity).capacity(initialCapacity);
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonCompositeBuffer(maxCapacity).append(
+            new ProtonNioByteBuffer(ByteBuffer.allocateDirect(initialCapacity), 0));
+    }
+
+    @Override
+    protected ProtonBuffer wrapBuffer(byte[] array) {
+        ProtonCompositeBuffer composite = new ProtonCompositeBuffer(Integer.MAX_VALUE);
+        return composite.append(ProtonByteBufferAllocator.DEFAULT.wrap(array));
+    }
+
+    private ProtonBuffer allocateBufferOfOffsetComposites(int capacity) {
+        ProtonBuffer buffer1 = new ProtonNioByteBuffer(ByteBuffer.allocate((capacity / 2) + 10)).skipBytes(10);
+        ProtonBuffer buffer2 = new ProtonNioByteBuffer(ByteBuffer.allocate((capacity / 2) + 10)).skipBytes(10);
+
+        return new ProtonCompositeBuffer().append(buffer1).append(buffer2).setWriteIndex(0);
+    }
+
+    private ProtonBuffer allocateDirectBufferOfOffsetComposites(int capacity) {
+        ProtonBuffer buffer1 = new ProtonNioByteBuffer(ByteBuffer.allocateDirect((capacity / 2) + 10)).skipBytes(10);
+        ProtonBuffer buffer2 = new ProtonNioByteBuffer(ByteBuffer.allocateDirect((capacity / 2) + 10)).skipBytes(10);
+
+        return new ProtonCompositeBuffer().append(buffer1).append(buffer2).setWriteIndex(0);
+    }
+
+    //----- Test Support Methods
+
+    private void assertContentEquals(ProtonBuffer source, ProtonBuffer other) {
+        assertEquals(source.capacity(), other.capacity());
+        for (int i = 0; i < source.capacity(); i++) {
+            assertEquals(source.getByte(i), other.getByte(i));
+        }
+    }
+
+    private void assertContentEquals(ProtonBuffer buffer, byte array[], int offset, int length) {
+        for (int i = 0; i < length; i++) {
+            assertEquals(buffer.getByte(i), array[offset + i]);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonDuplicatedBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonDuplicatedBufferTest.java
new file mode 100644
index 0000000..a7d3bfc
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonDuplicatedBufferTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+import io.netty.buffer.Unpooled;
+
+/**
+ * Test coverage for the duplicated buffer wrapper class.
+ */
+public class ProtonDuplicatedBufferTest extends ProtonAbstractBufferTest {
+
+    @Override
+    protected boolean canAllocateDirectBackedBuffers() {
+        return true;
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity) {
+        return new ProtonByteBuffer(initialCapacity).setWriteIndex(initialCapacity).duplicate().clear();
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.directBuffer(initialCapacity)).setWriteIndex(initialCapacity).duplicate().clear();
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonByteBuffer(initialCapacity, maxCapacity).setWriteIndex(initialCapacity).duplicate().clear();
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.directBuffer(initialCapacity, maxCapacity)).setWriteIndex(initialCapacity).duplicate().clear();
+    }
+
+    @Override
+    protected ProtonBuffer wrapBuffer(byte[] array) {
+        return new ProtonNioByteBuffer(ByteBuffer.wrap(array));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBufferTest.java
new file mode 100644
index 0000000..26d58ff
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonNettyByteBufferTest.java
@@ -0,0 +1,645 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.nio.ByteBuffer;
+
+import org.junit.jupiter.api.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+/**
+ * Test the buffer wrapper around Netty ByteBuf instances
+ */
+public class ProtonNettyByteBufferTest extends ProtonAbstractBufferTest {
+
+    private static final int CAPACITY = 4096; // Must be even
+    private static final int BLOCK_SIZE = 128;
+
+    public static final byte[] EMPTY_BYTES = {};
+
+    @Test
+    public void testUnwrap() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        assertSame(buffer, wrapper.unwrap());
+        ProtonBuffer duplicate = wrapper.duplicate();
+        assertTrue(duplicate instanceof ProtonNettyByteBuffer);
+        assertNotSame(((ProtonNettyByteBuffer) duplicate).unwrap(), buffer);
+    }
+
+    @Test
+    public void testReaderIndexBoundaryCheck4() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        wrapper.setWriteIndex(0);
+        wrapper.setReadIndex(0);
+        wrapper.setWriteIndex(buffer.capacity());
+        wrapper.setReadIndex(buffer.capacity());
+    }
+
+    @Test
+    public void testCreateWrapper() {
+        ByteBuf buffer = Unpooled.buffer();
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        assertEquals(buffer.capacity(), wrapper.capacity());
+        assertEquals(buffer.readableBytes(), wrapper.getReadableBytes());
+        assertEquals(buffer.writableBytes(), wrapper.getWritableBytes());
+        assertEquals(buffer.readerIndex(), wrapper.getReadIndex());
+        assertEquals(buffer.writerIndex(), wrapper.getWriteIndex());
+    }
+
+    @Test
+    public void testReadByteFromWrapper() {
+        ByteBuf buffer = Unpooled.buffer();
+
+        for (int i = 0; i < 256; ++i) {
+            buffer.writeByte(i);
+        }
+
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        assertEquals(buffer.capacity(), wrapper.capacity());
+        assertEquals(buffer.readableBytes(), wrapper.getReadableBytes());
+        assertEquals(buffer.writableBytes(), wrapper.getWritableBytes());
+        assertEquals(buffer.readerIndex(), wrapper.getReadIndex());
+        assertEquals(buffer.writerIndex(), wrapper.getWriteIndex());
+
+        for (int i = 0; i < 256; ++i) {
+            assertEquals((byte) i, wrapper.readByte());
+        }
+    }
+
+    @Test
+    public void testReadShortFromWrapper() {
+        ByteBuf buffer = Unpooled.buffer();
+
+        for (int i = 0; i < 256; ++i) {
+            buffer.writeShort(i);
+        }
+
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        assertEquals(buffer.capacity(), wrapper.capacity());
+        assertEquals(buffer.readableBytes(), wrapper.getReadableBytes());
+        assertEquals(buffer.writableBytes(), wrapper.getWritableBytes());
+        assertEquals(buffer.readerIndex(), wrapper.getReadIndex());
+        assertEquals(buffer.writerIndex(), wrapper.getWriteIndex());
+
+        for (int i = 0; i < 256; ++i) {
+            assertEquals((short) i, wrapper.readShort());
+        }
+    }
+
+    @Test
+    public void testReadIntFromWrapper() {
+        ByteBuf buffer = Unpooled.buffer();
+
+        for (int i = 0; i < 256; ++i) {
+            buffer.writeInt(i);
+        }
+
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        assertEquals(buffer.capacity(), wrapper.capacity());
+        assertEquals(buffer.readableBytes(), wrapper.getReadableBytes());
+        assertEquals(buffer.writableBytes(), wrapper.getWritableBytes());
+        assertEquals(buffer.readerIndex(), wrapper.getReadIndex());
+        assertEquals(buffer.writerIndex(), wrapper.getWriteIndex());
+
+        for (int i = 0; i < 256; ++i) {
+            assertEquals(i, wrapper.readInt());
+        }
+    }
+
+    @Test
+    public void testReadLongFromWrapper() {
+        ByteBuf buffer = Unpooled.buffer();
+
+        for (int i = 0; i < 256; ++i) {
+            buffer.writeLong(i);
+        }
+
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        assertEquals(buffer.capacity(), wrapper.capacity());
+        assertEquals(buffer.readableBytes(), wrapper.getReadableBytes());
+        assertEquals(buffer.writableBytes(), wrapper.getWritableBytes());
+        assertEquals(buffer.readerIndex(), wrapper.getReadIndex());
+        assertEquals(buffer.writerIndex(), wrapper.getWriteIndex());
+
+        for (int i = 0; i < 256; ++i) {
+            assertEquals(i, wrapper.readLong());
+        }
+    }
+
+    @Test
+    public void testSetReadIndexBoundaryCheckForNegative() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        try {
+            wrapper.setWriteIndex(0);
+        } catch (IndexOutOfBoundsException e) {
+            fail("Should be able to set index to zero");
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setReadIndex(-1));
+    }
+
+    @Test
+    public void testSetReadIndexBoundaryCheckForOverCapacityValue() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        try {
+            wrapper.setWriteIndex(buffer.capacity());
+        } catch (IndexOutOfBoundsException e) {
+            fail();
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setReadIndex(buffer.capacity() + 1));
+    }
+
+    @Test
+    public void setReadIndexBoundaryCheckValueBeyondWriteIndex() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        try {
+            wrapper.setWriteIndex(CAPACITY / 2);
+        } catch (IndexOutOfBoundsException e) {
+            fail();
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setReadIndex(CAPACITY * 3 / 2));
+    }
+
+    @Test
+    public void setWriteIndexBoundaryCheckValueBeyondCapacity() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        try {
+            wrapper.setWriteIndex(CAPACITY);
+            wrapper.setReadIndex(CAPACITY);
+        } catch (IndexOutOfBoundsException e) {
+            fail("Should be able to place indices at capacity");
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setWriteIndex(wrapper.capacity() + 1));
+    }
+
+    @Test
+    public void setWriteIndexBoundaryCheckWriteIndexBelowReadIndex() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        try {
+            wrapper.setWriteIndex(CAPACITY);
+            wrapper.setReadIndex(CAPACITY / 2);
+        } catch (IndexOutOfBoundsException e) {
+            fail("Should be able to place indices at capacity and half capacity");
+        }
+
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setWriteIndex(CAPACITY / 4));
+
+    }
+
+    @Test
+    public void testWriterIndexBoundaryCheckEmptyWriteDoesNotThrow() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+
+        wrapper.setWriteIndex(0);
+        wrapper.setReadIndex(0);
+        wrapper.setWriteIndex(CAPACITY);
+
+        wrapper.writeBytes(ByteBuffer.wrap(EMPTY_BYTES));
+    }
+
+    @Test
+    public void testGetBooleanBoundaryCheck1() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getBoolean(-1));
+    }
+
+    @Test
+    public void testGetBooleanBoundaryCheck2() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getBoolean(wrapper.capacity()));
+    }
+
+    @Test
+    public void testGetByteBoundaryCheck1() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getByte(-1));
+    }
+
+    @Test
+    public void testGetByteBoundaryCheck2() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getByte(wrapper.capacity()));
+    }
+
+    @Test
+    public void testGetShortBoundaryCheck1() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getShort(-1));
+    }
+
+    @Test
+    public void testGetShortBoundaryCheck2() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getShort(wrapper.capacity() - 1));
+    }
+
+    @Test
+    public void testGetIntBoundaryCheck1() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getInt(-1));
+    }
+
+    @Test
+    public void testGetIntBoundaryCheck2() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getInt(wrapper.capacity() - 3));
+    }
+
+    @Test
+    public void testGetLongBoundaryCheck1() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getLong(-1));
+    }
+
+    @Test
+    public void testGetLongBoundaryCheck2() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getLong(wrapper.capacity() - 7));
+    }
+
+    @Test
+    public void testGetByteArrayBoundaryCheck1() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getBytes(-1, EMPTY_BYTES));
+    }
+
+    @Test
+    public void testGetByteArrayBoundaryCheck2() {
+        ByteBuf buffer = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(buffer);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getBytes(-1, EMPTY_BYTES, 0, 0));
+    }
+
+    @Test
+    public void testGetByteArrayBoundaryCheckWithNegativeOffset() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+
+        byte[] dst = new byte[4];
+        wrapper.setInt(0, 0x01020304);
+        try {
+            wrapper.getBytes(0, dst, -1, 4);
+            fail("Should not allow offset out of range.");
+        } catch (IndexOutOfBoundsException e) {
+            // Success
+        }
+
+        // No partial copy is expected.
+        assertEquals(0, dst[0]);
+        assertEquals(0, dst[1]);
+        assertEquals(0, dst[2]);
+        assertEquals(0, dst[3]);
+    }
+
+    @Test
+    public void testGetByteArrayBoundaryCheckRangeOfWriteOutOfBounds() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+
+        byte[] dst = new byte[4];
+        wrapper.setInt(0, 0x01020304);
+        try {
+            wrapper.getBytes(0, dst, 1, 4);
+            fail("Should not allow get when range produces out of bounds write");
+        } catch (IndexOutOfBoundsException e) {
+            // Success
+        }
+
+        // No partial copy is expected.
+        assertEquals(0, dst[0]);
+        assertEquals(0, dst[1]);
+        assertEquals(0, dst[2]);
+        assertEquals(0, dst[3]);
+    }
+
+    @Test
+    public void testGetByteBufferBoundaryCheck() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.getBytes(-1, ByteBuffer.allocate(0)));
+    }
+
+    @Test
+    public void testCopyBoundaryCheck1() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.copy(-1, 0));
+    }
+
+    @Test
+    public void testCopyBoundaryCheck2() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.copy(0, wrapper.capacity() + 1));
+    }
+
+    @Test
+    public void testCopyBoundaryCheck3() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.copy(wrapper.capacity() + 1, 0));
+    }
+
+    @Test
+    public void testCopyBoundaryCheck4() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.copy(wrapper.capacity(), 1));
+    }
+
+    @Test
+    public void testSetIndexBoundaryCheck1() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setIndex(-1, CAPACITY));
+    }
+
+    @Test
+    public void testSetIndexBoundaryCheck2() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setIndex(CAPACITY / 2, CAPACITY / 4));
+    }
+
+    @Test
+    public void testSetIndexBoundaryCheck3() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+        assertThrows(IndexOutOfBoundsException.class, () -> wrapper.setIndex(0, CAPACITY + 1));
+    }
+
+    @Test
+    public void testGetByteBufferStateAfterLimtedGet() {
+        ByteBuffer dst = ByteBuffer.allocate(4);
+
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer wrapper = new ProtonNettyByteBuffer(netty);
+
+        dst.position(1);
+        dst.limit(3);
+
+        wrapper.setByte(0, (byte) 1);
+        wrapper.setByte(1, (byte) 2);
+        wrapper.setByte(2, (byte) 3);
+        wrapper.setByte(3, (byte) 4);
+        wrapper.getBytes(1, dst);
+
+        assertEquals(3, dst.position());
+        assertEquals(3, dst.limit());
+
+        dst.clear();
+        assertEquals(0, dst.get(0));
+        assertEquals(2, dst.get(1));
+        assertEquals(3, dst.get(2));
+        assertEquals(0, dst.get(3));
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer3() {
+        doTestRandomProtonBufferTransfer3(false);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer3DirectBackedBuffer() {
+        doTestRandomProtonBufferTransfer3(true);
+    }
+
+    private void doTestRandomProtonBufferTransfer3(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] valueContent = new byte[BLOCK_SIZE * 2];
+        ProtonBuffer value = new ProtonByteBuffer(valueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE * 2];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+        }
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer4() {
+        doTestRandomProtonBufferTransfer4(false);
+    }
+
+    @Test
+    public void testRandomProtonBufferTransfer4DirectBackedBuffer() {
+        doTestRandomProtonBufferTransfer4(true);
+    }
+
+    private void doTestRandomProtonBufferTransfer4(boolean direct) {
+        final ProtonBuffer buffer;
+        if (direct) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        byte[] valueContent = new byte[BLOCK_SIZE * 2];
+        ProtonNettyByteBuffer value = new ProtonNettyByteBuffer(Unpooled.wrappedBuffer(valueContent));
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[BLOCK_SIZE * 2];
+        ProtonNettyByteBuffer expectedValue = new ProtonNettyByteBuffer(Unpooled.wrappedBuffer(expectedValueContent));
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+        }
+    }
+
+    @Test
+    public void testRandomProtonNettyBufferTransfer2() {
+        doTestRandomProtonBufferTransfer2(false, false);
+    }
+
+    @Test
+    public void testRandomProtonNettyBufferTransfer2DirectSource() {
+        doTestRandomProtonBufferTransfer2(true, false);
+    }
+
+    @Test
+    public void testRandomProtonNettyBufferTransfer2DirectTarget() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomProtonBufferTransfer2(false, true);
+    }
+
+    @Test
+    public void testRandomProtonNettyBufferTransfer2DirectSourceAndTarget() {
+        assumeTrue(canAllocateDirectBackedBuffers());
+        doTestRandomProtonBufferTransfer2(true, true);
+    }
+
+    /*
+     * Tests getBytes with netty wrapper to netty wrapper with direct and non-direct variants
+     */
+    private void doTestRandomProtonBufferTransfer2(boolean directSource, boolean directTarget) {
+        final ProtonBuffer buffer;
+        if (directTarget) {
+            buffer = allocateDirectBuffer(LARGE_CAPACITY);
+        } else {
+            buffer = allocateBuffer(LARGE_CAPACITY);
+        }
+
+        final int SIZE = BLOCK_SIZE * 2;
+        byte[] valueContent = new byte[SIZE];
+        final ProtonBuffer value;
+        if (directSource) {
+            value = allocateDirectBuffer(SIZE);
+        } else {
+            value = allocateBuffer(SIZE);
+        }
+
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(valueContent);
+            value.clear();
+            value.writeBytes(valueContent);
+            buffer.setBytes(i, value, random.nextInt(BLOCK_SIZE), BLOCK_SIZE);
+        }
+
+        random.setSeed(seed);
+        byte[] expectedValueContent = new byte[SIZE];
+        ProtonBuffer expectedValue = new ProtonByteBuffer(expectedValueContent);
+        for (int i = 0; i < buffer.capacity() - BLOCK_SIZE + 1; i += BLOCK_SIZE) {
+            random.nextBytes(expectedValueContent);
+            int valueOffset = random.nextInt(BLOCK_SIZE);
+            buffer.getBytes(i, value, valueOffset, BLOCK_SIZE);
+            for (int j = valueOffset; j < valueOffset + BLOCK_SIZE; j ++) {
+                assertEquals(expectedValue.getByte(j), value.getByte(j));
+            }
+        }
+    }
+
+    @Test
+    public void testSkipBytes1() {
+        ByteBuf netty = Unpooled.buffer(CAPACITY);
+        ProtonNettyByteBuffer buffer = new ProtonNettyByteBuffer(netty);
+
+        buffer.setIndex(CAPACITY / 4, CAPACITY / 2);
+
+        buffer.skipBytes(CAPACITY / 4);
+        assertEquals(CAPACITY / 4 * 2, buffer.getReadIndex());
+
+        try {
+            buffer.skipBytes(CAPACITY / 4 + 1);
+            fail();
+        } catch (IndexOutOfBoundsException e) {
+            // Expected
+        }
+
+        // Should remain unchanged.
+        assertEquals(CAPACITY / 4 * 2, buffer.getReadIndex());
+    }
+
+    //----- Test API implemented for the abstract base class tests
+
+    @Override
+    protected boolean canAllocateDirectBackedBuffers() {
+        return true;
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.buffer(initialCapacity));
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.directBuffer(initialCapacity));
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.buffer(initialCapacity, maxCapacity));
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.directBuffer(initialCapacity, maxCapacity));
+    }
+
+    @Override
+    protected ProtonBuffer wrapBuffer(byte[] array) {
+        return new ProtonNettyByteBuffer(Unpooled.wrappedBuffer(array));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonNioByteBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonNioByteBufferTest.java
new file mode 100644
index 0000000..2829be9
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonNioByteBufferTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.ByteBuffer;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for the NIO ByteBuffer wrapper class
+ */
+public class ProtonNioByteBufferTest extends ProtonAbstractBufferTest {
+
+    //----- Test NIO buffer implementation specifics
+
+    @Test
+    public void testUnwrapAllocatedBuffer() {
+        ProtonBuffer buffer = allocateBuffer(13, 13);
+
+        ByteBuffer unwrapped = (ByteBuffer) buffer.unwrap();
+
+        assertEquals(13, unwrapped.capacity());
+        assertEquals(0, unwrapped.position());
+        assertEquals(13, unwrapped.limit());
+    }
+
+    @Test
+    public void testUnwrapWrappedArray() {
+        ProtonBuffer buffer = wrapBuffer(new byte[13]);
+
+        ByteBuffer unwrapped = (ByteBuffer) buffer.unwrap();
+
+        assertEquals(13, unwrapped.capacity());
+        assertEquals(0, unwrapped.position());
+        assertEquals(13, unwrapped.limit());
+    }
+
+    @Test
+    public void testUnwrapWrappedByteBuffer() {
+        ProtonBuffer buffer = new ProtonNioByteBuffer(ByteBuffer.allocate(13));
+
+        ByteBuffer unwrapped = (ByteBuffer) buffer.unwrap();
+
+        assertEquals(13, unwrapped.capacity());
+        assertEquals(0, unwrapped.position());
+        assertEquals(13, unwrapped.limit());
+    }
+
+    @Test
+    public void testUnwrapWrappedByteBufferWithWriteIndex() {
+        ProtonBuffer buffer = new ProtonNioByteBuffer(ByteBuffer.allocate(13), 13);
+
+        ByteBuffer unwrapped = (ByteBuffer) buffer.unwrap();
+
+        assertEquals(13, unwrapped.capacity());
+        assertEquals(0, unwrapped.position());
+        assertEquals(13, unwrapped.limit());
+    }
+
+    @Override
+    @Test
+    public void testCapacityEnforceMaxCapacity() {
+        ProtonBuffer buffer = allocateBuffer(13, 13);
+        assertEquals(13, buffer.maxCapacity());
+        assertEquals(13, buffer.capacity());
+        assertThrows(UnsupportedOperationException.class, () -> buffer.capacity(14));
+    }
+
+    @Override
+    @Test
+    public void testCapacityNegative() {
+        ProtonBuffer buffer = allocateBuffer(13, 13);
+        assertEquals(13, buffer.maxCapacity());
+        assertEquals(13, buffer.capacity());
+        assertThrows(IllegalArgumentException.class, () -> buffer.capacity(-1));
+    }
+
+    //----- Implement generic create methods from abstract test base
+
+    @Override
+    protected boolean canBufferCapacityBeChanged() {
+        return false;
+    }
+
+    @Override
+    protected boolean canAllocateDirectBackedBuffers() {
+        return true;
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity) {
+        return new ProtonNioByteBuffer(ByteBuffer.allocate(initialCapacity), 0);
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity) {
+        return new ProtonNioByteBuffer(ByteBuffer.allocateDirect(initialCapacity), 0);
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity) {
+        if (initialCapacity != maxCapacity) {
+            throw new UnsupportedOperationException("NIO buffer wrappers cannot grow");
+        }
+
+        return new ProtonNioByteBuffer(ByteBuffer.allocate(initialCapacity), 0);
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity) {
+        if (initialCapacity != maxCapacity) {
+            throw new UnsupportedOperationException("NIO buffer wrappers cannot grow");
+        }
+
+        return new ProtonNioByteBuffer(ByteBuffer.allocateDirect(initialCapacity), 0);
+    }
+
+    @Override
+    protected ProtonBuffer wrapBuffer(byte[] array) {
+        return new ProtonNioByteBuffer(ByteBuffer.wrap(array));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonSlicedBufferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonSlicedBufferTest.java
new file mode 100644
index 0000000..3ae4f84
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/ProtonSlicedBufferTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer;
+
+import java.nio.ByteBuffer;
+
+import io.netty.buffer.Unpooled;
+
+/**
+ * Tests that cover the usages of the sliced proton buffer class.
+ */
+public class ProtonSlicedBufferTest extends ProtonAbstractBufferTest {
+
+    @Override
+    protected boolean canBufferCapacityBeChanged() {
+        return false;
+    }
+
+    @Override
+    protected boolean canAllocateDirectBackedBuffers() {
+        return true;
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity) {
+        return new ProtonByteBuffer(initialCapacity).setWriteIndex(initialCapacity).slice().clear();
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity) {
+        return new ProtonNioByteBuffer(ByteBuffer.allocateDirect(initialCapacity), 0).setWriteIndex(initialCapacity).slice().clear();
+    }
+
+    @Override
+    protected ProtonBuffer allocateBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonByteBuffer(initialCapacity, maxCapacity).setWriteIndex(initialCapacity).slice().clear();
+    }
+
+    @Override
+    protected ProtonBuffer allocateDirectBuffer(int initialCapacity, int maxCapacity) {
+        return new ProtonNettyByteBuffer(Unpooled.directBuffer(initialCapacity, maxCapacity)).setWriteIndex(initialCapacity).slice().clear();
+    }
+
+    @Override
+    protected ProtonBuffer wrapBuffer(byte[] array) {
+        return new ProtonByteBuffer(array).slice();
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/util/ProtonTestByteBuffer.java b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/util/ProtonTestByteBuffer.java
new file mode 100644
index 0000000..7bf2bbd
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/buffer/util/ProtonTestByteBuffer.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.buffer.util;
+
+import org.apache.qpid.protonj2.buffer.ProtonByteBuffer;
+
+/**
+ * Used in testing of ProtonBuffers
+ */
+public class ProtonTestByteBuffer extends ProtonByteBuffer {
+
+    private boolean hasArray;
+
+    public ProtonTestByteBuffer() {
+        this(ProtonByteBuffer.DEFAULT_CAPACITY, ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, true);
+    }
+
+    public ProtonTestByteBuffer(boolean hasArray) {
+        this(ProtonByteBuffer.DEFAULT_CAPACITY, ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, hasArray);
+    }
+
+    public ProtonTestByteBuffer(int initialCapacity) {
+        this(initialCapacity, ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, true);
+    }
+
+    public ProtonTestByteBuffer(int initialCapacity, boolean hasArray) {
+        this(initialCapacity, ProtonByteBuffer.DEFAULT_MAXIMUM_CAPACITY, hasArray);
+    }
+
+    public ProtonTestByteBuffer(int initialCapacity, int maximumCapacity) {
+        super(initialCapacity, maximumCapacity);
+    }
+
+    public ProtonTestByteBuffer(int initialCapacity, int maximumCapacity, boolean hasArray) {
+        super(initialCapacity, maximumCapacity);
+
+        this.hasArray = hasArray;
+    }
+
+    public ProtonTestByteBuffer(byte[] array) {
+        super(array);
+    }
+
+    public ProtonTestByteBuffer(byte[] array, int maximumCapacity) {
+        super(array, maximumCapacity);
+    }
+
+    public ProtonTestByteBuffer(byte[] array, int maximumCapacity, int writeIndex) {
+        super(array, maximumCapacity, writeIndex);
+    }
+
+    @Override
+    public boolean hasArray() {
+        return hasArray ? hasArray() : false;
+    }
+
+    @Override
+    public
+    byte[] getArray() {
+        if (!hasArray) {
+            throw new UnsupportedOperationException();
+        }
+
+        return getArray();
+    }
+
+    @Override
+    public int getArrayOffset() {
+        if (!hasArray) {
+            throw new UnsupportedOperationException();
+        }
+
+        return getArrayOffset();
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/CodecTestSupport.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/CodecTestSupport.java
new file mode 100644
index 0000000..72b5b60
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/CodecTestSupport.java
@@ -0,0 +1,406 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+import org.apache.qpid.protonj2.codec.legacy.LegacyCodecAdapter;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Support class for tests of the type decoders
+ */
+public class CodecTestSupport {
+
+    protected static final int LARGE_SIZE = 1024;
+    protected static final int SMALL_SIZE = 32;
+
+    protected static final int LARGE_ARRAY_SIZE = 1024;
+    protected static final int SMALL_ARRAY_SIZE = 32;
+
+    protected DecoderState decoderState;
+    protected EncoderState encoderState;
+    protected Decoder decoder;
+    protected Encoder encoder;
+
+    protected StreamDecoderState streamDecoderState;
+    protected StreamDecoder streamDecoder;
+
+    protected Random random = new Random();
+    protected long currentSeed;
+
+    protected final LegacyCodecAdapter legacyCodec = new LegacyCodecAdapter();
+
+    @BeforeEach
+    public void setUp() {
+        decoder = ProtonDecoderFactory.create();
+        decoderState = decoder.newDecoderState();
+
+        encoder = ProtonEncoderFactory.create();
+        encoderState = encoder.newEncoderState();
+
+        streamDecoder = ProtonStreamDecoderFactory.create();
+        streamDecoderState = streamDecoder.newDecoderState();
+
+        currentSeed = System.nanoTime();
+        random.setSeed(currentSeed);
+    }
+
+    /**
+     * Compare a Open to another Open instance.
+     *
+     * @param open1
+     *      An {@link Open} instances or null
+     * @param open2
+     *      An {@link Open} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Open open1, Open open2) throws AssertionError {
+        if (open1 == null && open2 == null) {
+            return;
+        } else if (open1 == null || open2 == null) {
+            assertEquals(open1, open2);
+        }
+
+        assertEquals(open1.getChannelMax(), open2.getChannelMax(), "Channel max values not equal");
+        assertEquals(open1.getContainerId(), open2.getContainerId(), "Container Id values not equal");
+        assertEquals(open1.getHostname(), open2.getHostname(), "Hostname values not equal");
+        assertEquals(open1.getIdleTimeout(), open2.getIdleTimeout(), "Idle timeout values not equal");
+        assertEquals(open1.getMaxFrameSize(), open2.getMaxFrameSize(), "Max Frame Size values not equal");
+        assertEquals(open1.getProperties(), open2.getProperties(), "Properties Map values not equal");
+        assertArrayEquals(open1.getDesiredCapabilities(), open2.getDesiredCapabilities(), "Desired Capabilities are not equal");
+        assertArrayEquals(open1.getOfferedCapabilities(), open2.getOfferedCapabilities(), "Offered Capabilities are not equal");
+        assertArrayEquals(open1.getIncomingLocales(), open2.getIncomingLocales(), "Incoming Locales are not equal");
+        assertArrayEquals(open1.getOutgoingLocales(), open2.getOutgoingLocales(), "Outgoing Locales are not equal");
+    }
+
+    /**
+     * Compare a Close to another Close instance.
+     *
+     * @param close1
+     *      An {@link Close} instances or null
+     * @param close2
+     *      An {@link Close} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Close close1, Close close2) throws AssertionError {
+        if (close1 == null && close2 == null) {
+            return;
+        } else if (close1 == null || close2 == null) {
+            assertEquals(close1, close2);
+        }
+
+        assertTypesEqual(close1.getError(), close2.getError());
+    }
+
+    /**
+     * Compare a Begin to another Begin instance.
+     *
+     * @param begin1
+     *      A {@link Begin} instances or null
+     * @param begin2
+     *      A {@link Begin} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Begin begin1, Begin begin2) throws AssertionError {
+        if (begin1 == null && begin2 == null) {
+            return;
+        } else if (begin1 == null || begin2 == null) {
+            assertEquals(begin1, begin2);
+        }
+
+        assertSame(begin1.hasHandleMax(), begin2.hasHandleMax(), "Expected Begin with matching has handle max values");
+        assertEquals(begin1.getHandleMax(), begin2.getHandleMax(), "Handle max values not equal");
+
+        assertSame(begin1.hasIncomingWindow(), begin2.hasIncomingWindow(), "Expected Begin with matching has Incoming window values");
+        assertEquals(begin1.getIncomingWindow(), begin2.getIncomingWindow(), "Incoming Window values not equal");
+
+        assertSame(begin1.hasNextOutgoingId(), begin2.hasNextOutgoingId(), "Expected Begin with matching has Outgoing Id values");
+        assertEquals(begin1.getNextOutgoingId(), begin2.getNextOutgoingId(), "Outgoing Id values not equal");
+
+        assertSame(begin1.hasOutgoingWindow(), begin2.hasOutgoingWindow(), "Expected Begin with matching has Outgoing window values");
+        assertEquals(begin1.getOutgoingWindow(), begin2.getOutgoingWindow(), "Outgoing Window values not equal");
+
+        assertSame(begin1.hasRemoteChannel(), begin2.hasRemoteChannel(), "Expected Begin with matching has Remote Channel values");
+        assertEquals(begin1.getRemoteChannel(), begin2.getRemoteChannel(), "Remote Channel values not equal");
+
+        assertSame(begin1.hasProperties(), begin2.hasProperties(), "Expected Attach with matching has properties values");
+        assertEquals(begin1.getProperties(), begin2.getProperties(), "Properties Map values not equal");
+        assertSame(begin1.hasDesiredCapabilites(), begin2.hasDesiredCapabilites(), "Expected Attach with matching has desired capabilities values");
+        assertArrayEquals(begin1.getDesiredCapabilities(), begin2.getDesiredCapabilities(), "Desired Capabilities are not equal");
+        assertSame(begin2.hasOfferedCapabilites(), begin2.hasOfferedCapabilites(), "Expected Attach with matching has offered capabilities values");
+        assertArrayEquals(begin1.getOfferedCapabilities(), begin2.getOfferedCapabilities(), "Offered Capabilities are not equal");
+    }
+
+    /**
+     * Compare a Attach to another Attach instance.
+     *
+     * @param attach1
+     *      A {@link Attach} instances or null
+     * @param attach2
+     *      A {@link Attach} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Attach attach1, Attach attach2) throws AssertionError {
+        if (attach1 == attach2) {
+            return;
+        } else if (attach1 == null || attach2 == null) {
+            assertEquals(attach1, attach2);
+        }
+
+        assertSame(attach1.hasHandle(), attach2.hasHandle(), "Expected Attach with matching has handle values");
+        assertEquals(attach1.getHandle(), attach2.getHandle(), "Handle values not equal");
+
+        assertSame(attach1.hasInitialDeliveryCount(), attach2.hasInitialDeliveryCount(), "Expected Attach with matching has initial delivery count values");
+        assertEquals(attach1.getInitialDeliveryCount(), attach2.getInitialDeliveryCount(), "Initial delivery count values not equal");
+
+        assertSame(attach1.hasMaxMessageSize(), attach2.hasMaxMessageSize(), "Expected Attach with matching has max message size values");
+        assertEquals(attach1.getMaxMessageSize(), attach2.getMaxMessageSize(), "Max MessageSize values not equal");
+
+        assertSame(attach1.hasName(), attach2.hasName(), "Expected Attach with matching has name values");
+        assertEquals(attach1.getName(), attach2.getName(), "Link Name values not equal");
+
+        assertSame(attach1.hasSource(), attach2.hasSource(), "Expected Attach with matching has Source values");
+        assertTypesEqual(attach1.getSource(), attach2.getSource());
+        assertSame(attach1.hasTarget(), attach2.hasTarget(), "Expected Attach with matching has Target values");
+        Target attach1Target = attach1.getTarget();
+        Target attach2Target = attach2.getTarget();
+        assertTypesEqual(attach1Target, attach2Target);
+
+        assertSame(attach1.hasUnsettled(), attach2.hasUnsettled(), "Expected Attach with matching has handle values");
+        assertTypesEqual(attach1.getUnsettled(), attach2.getUnsettled());
+
+        assertSame(attach1.hasReceiverSettleMode(), attach2.hasReceiverSettleMode(), "Expected Attach with matching has receiver settle mode values");
+        assertEquals(attach1.getReceiverSettleMode(), attach2.getReceiverSettleMode(), "Receiver settle mode values not equal");
+
+        assertSame(attach1.hasSenderSettleMode(), attach2.hasSenderSettleMode(), "Expected Attach with matching has sender settle mode values");
+        assertEquals(attach1.getSenderSettleMode(), attach2.getSenderSettleMode(), "Sender settle mode values not equal");
+
+        assertSame(attach1.hasRole(), attach2.hasRole(), "Expected Attach with matching has Role values");
+        assertEquals(attach1.getRole(), attach2.getRole(), "Role values not equal");
+
+        assertSame(attach1.hasIncompleteUnsettled(), attach2.hasIncompleteUnsettled(), "Expected Attach with matching has incomplete unsettled values");
+        assertEquals(attach1.getIncompleteUnsettled(), attach2.getIncompleteUnsettled(), "Handle values not equal");
+
+        assertSame(attach1.hasProperties(), attach2.hasProperties(), "Expected Attach with matching has properties values");
+        assertEquals(attach1.getProperties(), attach2.getProperties(), "Properties Map values not equal");
+        assertSame(attach1.hasDesiredCapabilites(), attach2.hasDesiredCapabilites(), "Expected Attach with matching has desired capabilities values");
+        assertArrayEquals(attach1.getDesiredCapabilities(), attach2.getDesiredCapabilities(), "Desired Capabilities are not equal");
+        assertSame(attach1.hasOfferedCapabilites(), attach2.hasOfferedCapabilites(), "Expected Attach with matching has offered capabilities values");
+        assertArrayEquals(attach1.getOfferedCapabilities(), attach2.getOfferedCapabilities(), "Offered Capabilities are not equal");
+    }
+
+    /**
+     * Compare a Target to another Target instance.
+     *
+     * @param terminus1
+     *      A {@link Terminus} instances or null
+     * @param terminus2
+     *      A {@link Terminus} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Terminus terminus1, Terminus terminus2) throws AssertionError {
+        if (terminus1 == terminus2) {
+            return;
+        } else if (terminus1 == null || terminus2 == null) {
+            assertEquals(terminus1, terminus2);
+        } else if (terminus1.getClass().equals(terminus2.getClass())) {
+            fail("Terminus types are not equal");
+        }
+
+        if (terminus1 instanceof Source) {
+            assertTypesEqual((Source) terminus1, (Source) terminus2);
+        } else if (terminus1 instanceof Target) {
+            assertTypesEqual((Target) terminus1, (Target) terminus2);
+        } else if (terminus1 instanceof Coordinator) {
+            assertTypesEqual(terminus1, terminus2);
+        } else {
+            fail("Terminus types are of unknown origin.");
+        }
+    }
+
+    /**
+     * Compare a Target to another Target instance.
+     *
+     * @param target1
+     *      A {@link Target} instances or null
+     * @param target2
+     *      A {@link Target} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Target target1, Target target2) throws AssertionError {
+        if (target1 == target2) {
+            return;
+        } else if (target1 == null || target2 == null) {
+            assertEquals(target1, target2);
+        }
+
+        assertEquals(target1.getAddress(), target2.getAddress(), "Addrress values not equal");
+        assertEquals(target1.getDurable(), target2.getDurable(), "TerminusDurability values not equal");
+        assertEquals(target1.getExpiryPolicy(), target2.getExpiryPolicy(), "TerminusExpiryPolicy values not equal");
+        assertEquals(target1.getTimeout(), target2.getTimeout(), "Timeout values not equal");
+        assertEquals(target1.isDynamic(), target2.isDynamic(), "Dynamic values not equal");
+        assertEquals(target1.getDynamicNodeProperties(), target2.getDynamicNodeProperties(), "Dynamic Node Properties values not equal");
+        assertArrayEquals(target1.getCapabilities(), target2.getCapabilities(), "Capabilities values not equal");
+    }
+
+    /**
+     * Compare a Target to another Target instance.
+     *
+     * @param coordinator1
+     *      A {@link Coordinator} instances or null
+     * @param coordinator2
+     *      A {@link Coordinator} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Coordinator coordinator1, Coordinator coordinator2) throws AssertionError {
+        if (coordinator1 == coordinator2) {
+            return;
+        } else if (coordinator1 == null || coordinator2 == null) {
+            assertEquals(coordinator1, coordinator2);
+        }
+
+        assertArrayEquals(coordinator1.getCapabilities(), coordinator2.getCapabilities(), "Capabilities values not equal");
+    }
+
+    /**
+     * Compare a Source to another Source instance.
+     *
+     * @param source1
+     *      A {@link Source} instances or null
+     * @param source2
+     *      A {@link Source} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Source source1, Source source2) throws AssertionError {
+        if (source1 == source2) {
+            return;
+        } else if (source1 == null || source2 == null) {
+            assertEquals(source1, source2);
+        }
+
+        assertEquals(source1.getAddress(), source2.getAddress(), "Addrress values not equal");
+        assertEquals(source1.getDurable(), source2.getDurable(), "TerminusDurability values not equal");
+        assertEquals(source1.getExpiryPolicy(), source2.getExpiryPolicy(), "TerminusExpiryPolicy values not equal");
+        assertEquals(source1.getTimeout(), source2.getTimeout(), "Timeout values not equal");
+        assertEquals(source1.isDynamic(), source2.isDynamic(), "Dynamic values not equal");
+        assertEquals(source1.getDynamicNodeProperties(), source2.getDynamicNodeProperties(), "Dynamic Node Properties values not equal");
+        assertEquals(source1.getDistributionMode(), source2.getDistributionMode(), "Distribution Mode values not equal");
+        assertEquals(source1.getDefaultOutcome(), source2.getDefaultOutcome(), "Filter values not equal");
+        assertEquals(source1.getFilter(), source2.getFilter(), "Default outcome values not equal");
+        assertArrayEquals(source1.getOutcomes(), source2.getOutcomes(), "Outcomes values not equal");
+        assertArrayEquals(source1.getCapabilities(), source2.getCapabilities(), "Capabilities values not equal");
+    }
+
+    /**
+     * Compare a ErrorCondition to another ErrorCondition instance.
+     *
+     * @param condition1
+     *      A {@link ErrorCondition} instances or null
+     * @param condition2
+     *      A {@link ErrorCondition} instances or null.
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(ErrorCondition condition1, ErrorCondition condition2) throws AssertionError {
+        if (condition1 == condition2) {
+            return;
+        } else if (condition1 == null || condition2 == null) {
+            assertEquals(condition1, condition2);
+        }
+
+        assertEquals(condition1.getDescription(), condition2.getDescription(), "Error Descriptions should match");
+        assertEquals(condition1.getCondition(), condition2.getCondition(), "Error Condition should match");
+        assertEquals(condition1.getInfo(), condition2.getInfo(), "Error Info should match");
+    }
+
+    /**
+     * Compare a Unsettled Map to another Unsettled Map using the proton Binary type keys
+     * and DeliveryState values
+     *
+     * @param unsettled1
+     *      A {@link Map} instances or null.
+     * @param unsettled2
+     *      A {@link Map} instances or null
+     *
+     * @throws AssertionError
+     *      If the two types are not equal to one another.
+     */
+    public static void assertTypesEqual(Map<Binary, DeliveryState> unsettled1, Map<Binary, DeliveryState> unsettled2) throws AssertionError {
+        if (unsettled1 == null && unsettled2 == null) {
+            return;
+        } else if (unsettled1 == null || unsettled2 == null) {
+            assertEquals(unsettled1, unsettled2);
+        }
+
+        assertEquals(unsettled1.size(), unsettled2.size(), "Unsettled Map size values are not the same");
+
+        Iterator<Entry<Binary, DeliveryState>> legacyEntries = unsettled1.entrySet().iterator();
+        Iterator<Entry<Binary, DeliveryState>> unsettledEntries = unsettled2.entrySet().iterator();
+
+        while (legacyEntries.hasNext()) {
+            Entry<Binary, DeliveryState> legacyEntry = legacyEntries.next();
+            Entry<Binary, DeliveryState> unsettledEntry = unsettledEntries.next();
+
+            ByteBuffer legacyBuffer = legacyEntry.getKey().asByteBuffer();
+            ByteBuffer unsettledBuffer = unsettledEntry.getKey().asByteBuffer();
+
+            assertEquals(legacyBuffer, unsettledBuffer, "Delivery Tags do not match");
+            assertEquals(legacyEntry.getValue().getType(), unsettledEntry.getValue().getType(), "Delivery States do not match");
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/DeliveryTagCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/DeliveryTagCodecTest.java
new file mode 100644
index 0000000..377a49e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/DeliveryTagCodecTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.junit.jupiter.api.Test;
+
+public class DeliveryTagCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        try {
+            decoder.readDeliveryTag(buffer, decoderState);
+            fail("Should not allow read of integer type as this type");
+        } catch (DecodeException e) {}
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFromStream() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        try {
+            streamDecoder.readDeliveryTag(stream, streamDecoderState);
+            fail("Should not allow read of integer type as this type");
+        } catch (DecodeException e) {}
+    }
+
+    @Test
+    public void testReadDeliveryTagsFromBinaryEncodedValues() throws Exception {
+        testReadDeliveryTagsFromBinaryEncodedValues(false);
+    }
+
+    @Test
+    public void testReadDeliveryTagsFromBinaryEncodedValuesFS() throws Exception {
+        testReadDeliveryTagsFromBinaryEncodedValues(true);
+    }
+
+    public void testReadDeliveryTagsFromBinaryEncodedValues(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(32, 32);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final byte[] tagBytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertNull(streamDecoder.readDeliveryTag(stream, streamDecoderState));
+        } else {
+            assertNull(decoder.readDeliveryTag(buffer, decoderState));
+        }
+
+        buffer.writeByte(EncodingCodes.VBIN8);
+        buffer.writeByte(tagBytes.length);
+        buffer.writeBytes(tagBytes);
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(tagBytes.length);
+        buffer.writeBytes(tagBytes);
+
+        final DeliveryTag tag1;
+        final DeliveryTag tag2;
+
+        if (fromStream) {
+            tag1 = streamDecoder.readDeliveryTag(stream, streamDecoderState);
+            tag2 = streamDecoder.readDeliveryTag(stream, streamDecoderState);
+        } else {
+            tag1 = decoder.readDeliveryTag(buffer, decoderState);
+            tag2 = decoder.readDeliveryTag(buffer, decoderState);
+        }
+
+        assertNotSame(tag1, tag2);
+        assertArrayEquals(tag1.tagBytes(), tag2.tagBytes());
+        assertArrayEquals(tagBytes, tag1.tagBytes());
+        assertArrayEquals(tagBytes, tag2.tagBytes());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/RegisteredTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/RegisteredTypeCodecTest.java
new file mode 100644
index 0000000..c51e676
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/RegisteredTypeCodecTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.util.NoLocalType;
+import org.apache.qpid.protonj2.codec.util.NoLocalTypeDecoder;
+import org.apache.qpid.protonj2.codec.util.NoLocalTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling of type when the Decoder / Encoder is registered
+ */
+public class RegisteredTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testEncodeDecodeRegistredType() throws IOException {
+        doTestEncodeDecodeRegistredType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeRegistredTypeFromStream() throws IOException {
+        doTestEncodeDecodeRegistredType(true);
+    }
+
+    private void doTestEncodeDecodeRegistredType(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        // Register the codec pair.
+        encoder.registerDescribedTypeEncoder(new NoLocalTypeEncoder());
+        decoder.registerDescribedTypeDecoder(new NoLocalTypeDecoder());
+        streamDecoder.registerDescribedTypeDecoder(new NoLocalTypeDecoder());
+
+        encoder.writeObject(buffer, encoderState, NoLocalType.NO_LOCAL);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof NoLocalType);
+        NoLocalType resultTye = (NoLocalType) result;
+        assertEquals(NoLocalType.NO_LOCAL.getDescriptor(), resultTye.getDescriptor());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/UnknownDescribedTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/UnknownDescribedTypeCodecTest.java
new file mode 100644
index 0000000..85a9d74
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/UnknownDescribedTypeCodecTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.util.NoLocalType;
+import org.apache.qpid.protonj2.types.UnknownDescribedType;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the handling of UnknownDescribedType instances.
+ */
+public class UnknownDescribedTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecodeUnknownDescribedType() throws Exception {
+        doTestDecodeUnknownDescribedType(false);
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeFromStream() throws Exception {
+        doTestDecodeUnknownDescribedType(true);
+    }
+
+    private void doTestDecodeUnknownDescribedType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, NoLocalType.NO_LOCAL);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnknownDescribedType);
+        UnknownDescribedType resultTye = (UnknownDescribedType) result;
+        assertEquals(NoLocalType.NO_LOCAL.getDescriptor(), resultTye.getDescriptor());
+    }
+
+    @Test
+    public void testUnknownDescribedTypeInList() throws IOException {
+        doTestUnknownDescribedTypeInList(false);
+    }
+
+    @Test
+    public void testUnknownDescribedTypeInListFromStream() throws IOException {
+        doTestUnknownDescribedTypeInList(true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void doTestUnknownDescribedTypeInList(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<Object> listOfUnkowns = new ArrayList<>();
+
+        listOfUnkowns.add(NoLocalType.NO_LOCAL);
+
+        encoder.writeList(buffer, encoderState, listOfUnkowns);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof List);
+
+        final List<Object> decodedList = (List<Object>) result;
+        assertEquals(1, decodedList.size());
+
+        final Object listEntry = decodedList.get(0);
+        assertTrue(listEntry instanceof UnknownDescribedType);
+
+        UnknownDescribedType resultTye = (UnknownDescribedType) listEntry;
+        assertEquals(NoLocalType.NO_LOCAL.getDescriptor(), resultTye.getDescriptor());
+    }
+
+    @Test
+    public void testUnknownDescribedTypeInMap() throws IOException {
+        doTestUnknownDescribedTypeInMap(false);
+    }
+
+    @Test
+    public void testUnknownDescribedTypeInMapFromStream() throws IOException {
+        doTestUnknownDescribedTypeInMap(true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void doTestUnknownDescribedTypeInMap(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Object, Object> mapOfUnknowns = new HashMap<>();
+
+        mapOfUnknowns.put(NoLocalType.NO_LOCAL.getDescriptor(), NoLocalType.NO_LOCAL);
+
+        encoder.writeMap(buffer, encoderState, mapOfUnknowns);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Map);
+
+        final Map<Object, Object> decodedMap = (Map<Object, Object>) result;
+        assertEquals(1, decodedMap.size());
+
+        final Object mapEntry = decodedMap.get(NoLocalType.NO_LOCAL.getDescriptor());
+        assertTrue(mapEntry instanceof UnknownDescribedType);
+
+        UnknownDescribedType resultTye = (UnknownDescribedType) mapEntry;
+        assertEquals(NoLocalType.NO_LOCAL.getDescriptor(), resultTye.getDescriptor());
+    }
+
+    @Test
+    public void testUnknownDescribedTypeInArray() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        NoLocalType[] arrayOfUnknown = new NoLocalType[1];
+
+        arrayOfUnknown[0] = NoLocalType.NO_LOCAL;
+
+        try {
+            encoder.writeArray(buffer, encoderState, arrayOfUnknown);
+            fail("Should not be able to write an array of unregistered described type");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            encoder.writeObject(buffer, encoderState, arrayOfUnknown);
+            fail("Should not be able to write an array of unregistered described type");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnknownDescribedTypes() throws IOException {
+        doTestDecodeUnknownDescribedTypeSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnknownDescribedTypes() throws IOException {
+        doTestDecodeUnknownDescribedTypeSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnknownDescribedTypesFromStream() throws IOException {
+        doTestDecodeUnknownDescribedTypeSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnknownDescribedTypesFromStream() throws IOException {
+        doTestDecodeUnknownDescribedTypeSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeUnknownDescribedTypeSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, NoLocalType.NO_LOCAL);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof UnknownDescribedType);
+
+            UnknownDescribedType resultTye = (UnknownDescribedType) result;
+            assertEquals(NoLocalType.NO_LOCAL.getDescriptor(), resultTye.getDescriptor());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/benchmark/Benchmark.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/benchmark/Benchmark.java
new file mode 100644
index 0000000..e101249
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/benchmark/Benchmark.java
@@ -0,0 +1,422 @@
+/*
+ *
+ * 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.
+ *
+ */
+package org.apache.qpid.protonj2.codec.benchmark;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+
+public class Benchmark implements Runnable {
+
+    private static final int ITERATIONS = 10 * 1024 * 1024;
+
+    ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(8192);
+    private BenchmarkResult resultSet = new BenchmarkResult();
+    private boolean warming = true;
+
+    private Encoder encoder = CodecFactory.getDefaultEncoder();
+    private EncoderState encoderState = encoder.newEncoderState();
+    private Decoder decoder = CodecFactory.getDefaultDecoder();
+    private DecoderState decoderState = decoder.newDecoderState();
+
+    public static final void main(String[] args) throws IOException, InterruptedException {
+        System.out.println("Current PID: " + ManagementFactory.getRuntimeMXBean().getName());
+        Benchmark benchmark = new Benchmark();
+        benchmark.run();
+    }
+
+    @Override
+    public void run() {
+        try {
+            doBenchmarks();
+            warming = false;
+            doBenchmarks();
+        } catch (IOException e) {
+            System.out.println("Unexpected error: " + e.getMessage());
+        }
+    }
+
+    private void time(String message, BenchmarkResult resultSet) {
+        if (!warming) {
+            System.out.println("Benchamrk of type: " + message + ": ");
+            System.out.println("    Encode time = " + resultSet.getEncodeTimeMills());
+            System.out.println("    Decode time = " + resultSet.getDecodeTimeMills());
+        }
+    }
+
+    private final void doBenchmarks() throws IOException {
+        benchmarkListOfInts();
+        benchmarkUUIDs();
+        benchmarkHeader();
+        benchmarkProperties();
+        benchmarkMessageAnnotations();
+        benchmarkApplicationProperties();
+        benchmarkSymbols();
+        benchmarkTransfer();
+        benchmarkFlow();
+        benchmarkDisposition();
+        benchmarkString();
+        benchmarkData();
+        warming = false;
+    }
+
+    private void benchmarkListOfInts() throws IOException {
+        ArrayList<Object> list = new ArrayList<>(10);
+        for (int j = 0; j < 10; j++) {
+            list.add(0);
+        }
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeList(buffer, encoderState, list);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readList(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("List<Integer>", resultSet);
+    }
+
+    private void benchmarkUUIDs() throws IOException {
+        UUID uuid = UUID.randomUUID();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeUUID(buffer, encoderState, uuid);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readUUID(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("UUID", resultSet);
+    }
+
+    private void benchmarkTransfer() throws IOException {
+        Transfer transfer = new Transfer();
+        transfer.setDeliveryTag(new byte[] {1, 2, 3});
+        transfer.setHandle(1024);
+        transfer.setMessageFormat(0);
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, transfer);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Transfer", resultSet);
+    }
+
+    private void benchmarkFlow() throws IOException {
+        Flow flow = new Flow();
+        flow.setNextIncomingId(1);
+        flow.setIncomingWindow(2047);
+        flow.setNextOutgoingId(1);
+        flow.setOutgoingWindow(Integer.MAX_VALUE);
+        flow.setHandle(UnsignedInteger.ZERO.longValue());
+        flow.setDeliveryCount(10);
+        flow.setLinkCredit(1000);
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, flow);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Flow", resultSet);
+    }
+
+    private void benchmarkHeader() throws IOException {
+        Header header = new Header();
+        header.setDurable(true);
+        header.setFirstAcquirer(true);
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, header);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Header", resultSet);
+    }
+
+    private void benchmarkProperties() throws IOException {
+        Properties properties = new Properties();
+        properties.setTo("queue:1-1024");
+        properties.setReplyTo("queue:1-11024-reply");
+        properties.setMessageId("ID:255f1297-5a71-4df1-8147-b2cdf850a56f:1");
+        properties.setCreationTime(System.currentTimeMillis());
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Properties", resultSet);
+    }
+
+    private void benchmarkMessageAnnotations() throws IOException {
+        MessageAnnotations annotations = new MessageAnnotations(new HashMap<>());
+        annotations.getValue().put(Symbol.valueOf("test1"), UnsignedByte.valueOf((byte) 128));
+        annotations.getValue().put(Symbol.valueOf("test2"), UnsignedShort.valueOf((short) 128));
+        annotations.getValue().put(Symbol.valueOf("test3"), UnsignedInteger.valueOf((byte) 128));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, annotations);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("MessageAnnotations", resultSet);
+    }
+
+    private void benchmarkApplicationProperties() throws IOException {
+        ApplicationProperties properties = new ApplicationProperties(new HashMap<>());
+        properties.getValue().put("test1", UnsignedByte.valueOf((byte) 128));
+        properties.getValue().put("test2", UnsignedShort.valueOf((short) 128));
+        properties.getValue().put("test3", UnsignedInteger.valueOf((byte) 128));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("ApplicationProperties", resultSet);
+    }
+
+    private void benchmarkSymbols() throws IOException {
+        Symbol symbol1 = Symbol.valueOf("Symbol-1");
+        Symbol symbol2 = Symbol.valueOf("Symbol-2");
+        Symbol symbol3 = Symbol.valueOf("Symbol-3");
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeSymbol(buffer, encoderState, symbol1);
+            encoder.writeSymbol(buffer, encoderState, symbol2);
+            encoder.writeSymbol(buffer, encoderState, symbol3);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readSymbol(buffer, decoderState);
+            decoder.readSymbol(buffer, decoderState);
+            decoder.readSymbol(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Symbol", resultSet);
+    }
+
+    private void benchmarkString() throws IOException {
+        String string1 = new String("String-1-somewhat-long-test-to-validate-performance-improvements-to-the-proton-j-codec-@!%$");
+        String string2 = new String("String-2-somewhat-long-test-to-validate-performance-improvements-to-the-proton-j-codec-@!%$");
+        String string3 = new String("String-3-somewhat-long-test-to-validate-performance-improvements-to-the-proton-j-codec-@!%$");
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeString(buffer, encoderState, string1);
+            encoder.writeString(buffer, encoderState, string2);
+            encoder.writeString(buffer, encoderState, string3);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readString(buffer, decoderState);
+            decoder.readString(buffer, decoderState);
+            decoder.readString(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("String", resultSet);
+    }
+
+    private void benchmarkDisposition() throws IOException {
+        Disposition disposition = new Disposition();
+        disposition.setRole(Role.RECEIVER);
+        disposition.setSettled(true);
+        disposition.setState(Accepted.getInstance());
+        disposition.setFirst(2);
+        disposition.setLast(2);
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, disposition);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Disposition", resultSet);
+    }
+
+    private void benchmarkData() throws IOException {
+        Data data1 = new Data(new Binary(new byte[] {1, 2, 3}));
+        Data data2 = new Data(new Binary(new byte[] {4, 5, 6}));
+        Data data3 = new Data(new Binary(new byte[] {7, 8, 9}));
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.clear();
+            encoder.writeObject(buffer, encoderState, data1);
+            encoder.writeObject(buffer, encoderState, data2);
+            encoder.writeObject(buffer, encoderState, data3);
+        }
+        resultSet.encodesComplete();
+
+        resultSet.start();
+        for (int i = 0; i < ITERATIONS; i++) {
+            buffer.setReadIndex(0);
+            decoder.readObject(buffer, decoderState);
+            decoder.readObject(buffer, decoderState);
+            decoder.readObject(buffer, decoderState);
+        }
+        resultSet.decodesComplete();
+
+        time("Data", resultSet);
+    }
+
+    private static class BenchmarkResult {
+
+        private long startTime;
+
+        private long encodeTime;
+        private long decodeTime;
+
+        public void start() {
+            startTime = System.nanoTime();
+        }
+
+        public void encodesComplete() {
+            encodeTime = System.nanoTime() - startTime;
+        }
+
+        public void decodesComplete() {
+            decodeTime = System.nanoTime() - startTime;
+        }
+
+        public long getEncodeTimeMills() {
+            return TimeUnit.NANOSECONDS.toMillis(encodeTime);
+        }
+
+        public long getDecodeTimeMills() {
+            return TimeUnit.NANOSECONDS.toMillis(decodeTime);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderTest.java
new file mode 100644
index 0000000..5940d67
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/decoders/ProtonDecoderTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeEOFException;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.types.UnknownDescribedType;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.junit.jupiter.api.Test;
+
+public class ProtonDecoderTest extends CodecTestSupport {
+
+    @Test
+    public void testGetCachedDecoderStateReturnsCachedState() {
+        DecoderState first = decoder.getCachedDecoderState();
+
+        assertSame(first, decoder.getCachedDecoderState());
+    }
+
+    @Test
+    public void testReadNullFromReadObjectForNullEncodng() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertNull(decoder.readObject(buffer, decoderState));
+        assertNull(decoder.readObject(buffer, decoderState, UUID.class));
+    }
+
+    @Test
+    public void testTryReadFromEmptyBuffer() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should fail on read of object from empty buffer");
+        } catch (DecodeEOFException dex) {}
+    }
+
+    @Test
+    public void testErrorOnReadOfUnknownEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(255);
+
+        assertNull(decoder.peekNextTypeDecoder(buffer, decoderState));
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should throw if no type decoder exists for given type");
+        } catch (DecodeException ioe) {}
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        try {
+            decoder.readObject(buffer, decoderState, String.class);
+            fail("Should not allow for conversion to String type");
+        } catch (ClassCastException cce) {
+        }
+    }
+
+    @Test
+    public void testReadMultipleFromNullEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertNull(decoder.readMultiple(buffer, decoderState, UUID.class));
+    }
+
+    @Test
+    public void testReadMultipleFromSingleEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        UUID[] result = decoder.readMultiple(buffer, decoderState, UUID.class);
+
+        assertNotNull(result);
+        assertEquals(1, result.length);
+        assertEquals(value, result[0]);
+    }
+
+    @Test
+    public void testReadMultipleRequestsWrongTypeForArray() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        try {
+            decoder.readMultiple(buffer, decoderState, String.class);
+            fail("Should not be able to convert to wrong resulting array type");
+        } catch (ClassCastException cce) {}
+    }
+
+    @Test
+    public void testReadMultipleRequestsWrongTypeForArrayEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID[] value = new UUID[] { UUID.randomUUID(), UUID.randomUUID() };
+
+        encoder.writeArray(buffer, encoderState, value);
+
+        try {
+            decoder.readMultiple(buffer, decoderState, String.class);
+            fail("Should not be able to convert to wrong resulting array type");
+        } catch (ClassCastException cce) {}
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeWithNegativeLongDescriptor() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(UnsignedLong.MAX_VALUE.longValue());
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnknownDescribedType);
+
+        UnknownDescribedType type = (UnknownDescribedType) result;
+        assertTrue(type.getDescribed() instanceof UUID);
+        assertEquals(value, type.getDescribed());
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeWithMaxLongDescriptor() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(Long.MAX_VALUE);
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnknownDescribedType);
+
+        UnknownDescribedType type = (UnknownDescribedType) result;
+        assertTrue(type.getDescribed() instanceof UUID);
+        assertEquals(value, type.getDescribed());
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeWithUnknownDescriptorCode() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(255);
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnknownDescribedType);
+
+        UnknownDescribedType type = (UnknownDescribedType) result;
+        assertTrue(type.getDescribed() instanceof UUID);
+        assertEquals(value, type.getDescribed());
+        assertNotNull(type.toString());
+    }
+
+    @Test
+    public void testReadUnsignedIntegerTypes() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.UINT0);
+        buffer.writeByte(EncodingCodes.SMALLUINT);
+        buffer.writeByte(127);
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(0);
+        buffer.writeByte(0);
+        buffer.writeByte(0);
+        buffer.writeByte(255);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertEquals(0, decoder.readUnsignedInteger(buffer, decoderState, 32));
+        assertEquals(127, decoder.readUnsignedInteger(buffer, decoderState, 32));
+        assertEquals(255, decoder.readUnsignedInteger(buffer, decoderState, 32));
+        assertEquals(32, decoder.readUnsignedInteger(buffer, decoderState, 32));
+    }
+
+    @Test
+    public void testReadStringWithCustomStringDecoder() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.STR32);
+        buffer.writeInt(16);
+        buffer.writeBytes(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 });
+
+        ((ProtonDecoderState) decoderState).setStringDecoder(new UTF8Decoder() {
+
+            @Override
+            public String decodeUTF8(ProtonBuffer buffer, int utf8length) {
+               return "string-decoder";
+            }
+        });
+
+        assertNotNull(((ProtonDecoderState) decoderState).getStringDecoder());
+
+        String result = decoder.readString(buffer, decoderState);
+
+        assertEquals("string-decoder", result);
+        assertFalse(buffer.isReadable());
+    }
+
+    @Test
+    public void testStringReadFromCustomDecoderThrowsDecodeExceptionOnError() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.STR32);
+        buffer.writeInt(16);
+        buffer.writeBytes(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 });
+
+        ((ProtonDecoderState) decoderState).setStringDecoder(new UTF8Decoder() {
+
+            @Override
+            public String decodeUTF8(ProtonBuffer buffer, int utf8length) {
+                throw new IndexOutOfBoundsException();
+            }
+        });
+
+        assertNotNull(((ProtonDecoderState) decoderState).getStringDecoder());
+        assertThrows(DecodeException.class, () -> decoder.readString(buffer, decoderState));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderTest.java
new file mode 100644
index 0000000..ede6de7
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/decoders/ProtonStreamDecoderTest.java
@@ -0,0 +1,442 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.decoders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeEOFException;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.types.UnknownDescribedType;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class ProtonStreamDecoderTest extends CodecTestSupport {
+
+    @Test
+    public void testGetCachedDecoderStateReturnsCachedState() {
+        StreamDecoderState first = streamDecoder.getCachedDecoderState();
+
+        assertSame(first, streamDecoder.getCachedDecoderState());
+    }
+
+    @Test
+    public void testReadNullFromReadObjectForNullEncodng() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertNull(streamDecoder.readObject(stream, streamDecoderState));
+        assertNull(streamDecoder.readObject(stream, streamDecoderState, UUID.class));
+    }
+
+    @Test
+    public void testTryReadFromEmptyStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        try {
+            streamDecoder.readObject(stream, streamDecoderState);
+            fail("Should fail on read of object from empty stream");
+        } catch (DecodeEOFException dex) {}
+    }
+
+    @Test
+    public void testErrorOnReadOfUnknownEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(255);
+
+        assertNull(streamDecoder.peekNextTypeDecoder(stream, streamDecoderState));
+
+        try {
+            streamDecoder.readObject(stream, streamDecoderState);
+            fail("Should throw if no type streamDecoder exists for given type");
+        } catch (DecodeException ioe) {}
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        try {
+            streamDecoder.readObject(stream, streamDecoderState, String.class);
+            fail("Should not allow for conversion to String type");
+        } catch (ClassCastException cce) {
+        }
+    }
+
+    @Test
+    public void testReadMultipleFromNullEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertNull(streamDecoder.readMultiple(stream, streamDecoderState, UUID.class));
+    }
+
+    @Test
+    public void testReadMultipleFromSingleEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        UUID[] result = streamDecoder.readMultiple(stream, streamDecoderState, UUID.class);
+
+        assertNotNull(result);
+        assertEquals(1, result.length);
+        assertEquals(value, result[0]);
+    }
+
+    @Test
+    public void testReadMultipleRequestsWrongTypeForArray() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        try {
+            streamDecoder.readMultiple(stream, streamDecoderState, String.class);
+            fail("Should not be able to convert to wrong resulting array type");
+        } catch (ClassCastException cce) {}
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeWithNegativeLongDescriptor() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(UnsignedLong.MAX_VALUE.longValue());
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnknownDescribedType);
+
+        UnknownDescribedType type = (UnknownDescribedType) result;
+        assertTrue(type.getDescribed() instanceof UUID);
+        assertEquals(value, type.getDescribed());
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeWithMaxLongDescriptor() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(Long.MAX_VALUE);
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnknownDescribedType);
+
+        UnknownDescribedType type = (UnknownDescribedType) result;
+        assertTrue(type.getDescribed() instanceof UUID);
+        assertEquals(value, type.getDescribed());
+    }
+
+    @Test
+    public void testDecodeUnknownDescribedTypeWithUnknownDescriptorCode() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(255);
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnknownDescribedType);
+
+        UnknownDescribedType type = (UnknownDescribedType) result;
+        assertTrue(type.getDescribed() instanceof UUID);
+        assertEquals(value, type.getDescribed());
+        assertNotNull(type.toString());
+    }
+
+    @Test
+    public void testReadUnsignedIntegerTypes() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT0);
+        buffer.writeByte(EncodingCodes.SMALLUINT);
+        buffer.writeByte(127);
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(0);
+        buffer.writeByte(0);
+        buffer.writeByte(0);
+        buffer.writeByte(255);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertEquals(0, streamDecoder.readUnsignedInteger(stream, streamDecoderState, 32));
+        assertEquals(127, streamDecoder.readUnsignedInteger(stream, streamDecoderState, 32));
+        assertEquals(255, streamDecoder.readUnsignedInteger(stream, streamDecoderState, 32));
+        assertEquals(32, streamDecoder.readUnsignedInteger(stream, streamDecoderState, 32));
+    }
+
+    @Test
+    public void testReadMultipleRequestsWrongTypeForArrayEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID[] value = new UUID[] { UUID.randomUUID(), UUID.randomUUID() };
+
+        encoder.writeArray(buffer, encoderState, value);
+
+        try {
+            streamDecoder.readMultiple(stream, streamDecoderState, String.class);
+            fail("Should not be able to convert to wrong resulting array type");
+        } catch (ClassCastException cce) {}
+    }
+
+    @Test
+    public void testReadStringWithCustomStringDecoder() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.STR32);
+        buffer.writeInt(16);
+        buffer.writeBytes(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 });
+
+        ((ProtonStreamDecoderState) streamDecoderState).setStringDecoder(new UTF8StreamDecoder() {
+
+            @Override
+            public String decodeUTF8(InputStream tream) {
+                return "string-decoder";
+            }
+        });
+
+        assertNotNull(((ProtonStreamDecoderState) streamDecoderState).getStringDecoder());
+
+        String result = streamDecoder.readString(stream, streamDecoderState);
+
+        assertEquals("string-decoder", result);
+        assertTrue(buffer.isReadable());  // We didn't read anything so buffer was untouched
+    }
+
+    @Test
+    public void testStringReadFromCustomDecoderThrowsDecodeExceptionOnError() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.STR32);
+        buffer.writeInt(16);
+        buffer.writeBytes(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 });
+
+        ((ProtonStreamDecoderState) streamDecoderState).setStringDecoder(new UTF8StreamDecoder() {
+
+            @Override
+            public String decodeUTF8(InputStream tream) {
+                throw new IndexOutOfBoundsException();
+            }
+        });
+
+        assertNotNull(((ProtonStreamDecoderState) streamDecoderState).getStringDecoder());
+        assertThrows(DecodeException.class, () -> streamDecoder.readString(stream, streamDecoderState));
+    }
+
+    @Test
+    public void testCannotPeekFromStreamThatCannotMark() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        try {
+            streamDecoder.peekNextTypeDecoder(stream, streamDecoderState);
+            fail("Should fail on read of object from empty stream");
+        } catch (UnsupportedOperationException uopex) {}
+    }
+
+    @Test
+    public void testDecodeErrorFromPeekWhenStreamResetFails() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        encoder.writeObject(buffer, encoderState, "test");
+
+        Mockito.doThrow(new IOException()).when(stream).reset();
+
+        try {
+            streamDecoder.peekNextTypeDecoder(stream, streamDecoderState);
+            fail("Should fail on read of object from empty stream");
+        } catch (DecodeException dex) {}
+    }
+
+    @Test
+    public void testStreamDecoderCanStillReadWhenMarkNotSupported() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        encoder.writeObject(buffer, encoderState, "test");
+        encoder.writeObject(buffer, encoderState, Accepted.getInstance());
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        final Object string = streamDecoder.readObject(stream, streamDecoderState);
+        final Object accepted = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertEquals("test", string);
+        assertSame(Accepted.getInstance(), accepted);
+    }
+
+    @Test
+    public void testReadDescriptorOfTypeSymbol32MarkNotSupported() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SYM32);
+        buffer.writeInt(Accepted.DESCRIPTOR_SYMBOL.getLength());
+        Accepted.DESCRIPTOR_SYMBOL.writeTo(buffer);
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        final Object accepted = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertSame(Accepted.getInstance(), accepted);
+    }
+
+    @Test
+    public void testReadDescriptorOfTypeSymbol8MarkNotSupported() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SYM8);
+        buffer.writeByte(Accepted.DESCRIPTOR_SYMBOL.getLength());
+        Accepted.DESCRIPTOR_SYMBOL.writeTo(buffer);
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        final Object accepted = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertSame(Accepted.getInstance(), accepted);
+    }
+
+    @Test
+    public void testReadDescriptorOfTypeUnsignedLongMarkNotSupported() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(Accepted.DESCRIPTOR_CODE.longValue());
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        final Object accepted = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertSame(Accepted.getInstance(), accepted);
+    }
+
+    @Test
+    public void testReadDescriptorOfTypeSmallUnsignedLongMarkNotSupported() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        final Object accepted = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertSame(Accepted.getInstance(), accepted);
+    }
+
+    @Test
+    public void testDecodeErrorWhenMarkNotSupportedAndUnkownEncodingCodeFoundInDescribedType() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = Mockito.spy(new ProtonBufferInputStream(buffer));
+
+        Mockito.when(stream.markSupported()).thenReturn(false);
+
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(Accepted.DESCRIPTOR_CODE.longValue());
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        try {
+            streamDecoder.readObject(stream, streamDecoderState);
+            fail("Should fail on read of object with bad descriptor type");
+        } catch (DecodeException dex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderTest.java
new file mode 100644
index 0000000..8ebd332
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/encoders/ProtonEncoderTest.java
@@ -0,0 +1,408 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.encoders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.junit.jupiter.api.Test;
+
+class ProtonEncoderTest extends CodecTestSupport {
+
+    @Test
+    public void testCachedEncoderStateIsCached() throws IOException {
+        EncoderState state1 = encoder.getCachedEncoderState();
+        EncoderState state2 = encoder.getCachedEncoderState();
+
+        assertTrue(state1 instanceof ProtonEncoderState);
+        assertTrue(state1 instanceof ProtonEncoderState);
+
+        assertSame(state1, state2);
+    }
+
+    @Test
+    public void testProtonEncoderStateHasNoStringEncoderByDefault() throws IOException {
+        ProtonEncoderState state = (ProtonEncoderState) encoder.getCachedEncoderState();
+
+        assertNull(state.getUTF8Encoder());
+    }
+
+    @Test
+    public void testUseCustomUTF8EncoderInEncoderState() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        final String expected = "test-encoding-string";
+
+        ((ProtonEncoderState) encoderState).setUTF8Encoder(new UTF8Encoder() {
+
+            @Override
+            public ProtonBuffer encodeUTF8(ProtonBuffer buffer, CharSequence sequence) {
+                return buffer.writeBytes(sequence.toString().getBytes(StandardCharsets.UTF_8));
+            }
+        });
+
+        encoder.writeString(buffer, encoderState, expected);
+
+        final String result = decoder.readString(buffer, decoderState);
+
+        assertEquals(expected, result);
+    }
+
+    @Test
+    public void testWriteBooleanObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeBoolean(buffer, encoderState, Boolean.TRUE);
+        encoder.writeBoolean(buffer, encoderState, (Boolean) null);
+        encoder.writeBoolean(buffer, encoderState, Boolean.FALSE);
+
+        assertEquals(3, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.BOOLEAN_TRUE);
+        assertEquals(buffer.getByte(1), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(2), EncodingCodes.BOOLEAN_FALSE);
+    }
+
+    @Test
+    public void testWriteBooleanPrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeBoolean(buffer, encoderState, true);
+        encoder.writeBoolean(buffer, encoderState, false);
+
+        assertEquals(2, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.BOOLEAN_TRUE);
+        assertEquals(buffer.getByte(1), EncodingCodes.BOOLEAN_FALSE);
+    }
+
+    @Test
+    public void testWriteUnsignedByteObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedByte(buffer, encoderState, UnsignedByte.valueOf((byte) 0));
+        encoder.writeUnsignedByte(buffer, encoderState, (UnsignedByte) null);
+        encoder.writeUnsignedByte(buffer, encoderState, UnsignedByte.valueOf((byte) 255));
+
+        assertEquals(5, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.UBYTE);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(3), EncodingCodes.UBYTE);
+        assertEquals(buffer.getByte(4), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedBytePrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedByte(buffer, encoderState, (byte) 0);
+        encoder.writeUnsignedByte(buffer, encoderState, (byte) 255);
+
+        assertEquals(4, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.UBYTE);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), EncodingCodes.UBYTE);
+        assertEquals(buffer.getByte(3), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedShortObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedShort(buffer, encoderState, UnsignedShort.valueOf((short) 0));
+        encoder.writeUnsignedShort(buffer, encoderState, (UnsignedShort) null);
+        encoder.writeUnsignedShort(buffer, encoderState, UnsignedShort.valueOf((short) 65535));
+
+        assertEquals(7, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.USHORT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), 0);
+        assertEquals(buffer.getByte(3), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(4), EncodingCodes.USHORT);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedShortPrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedShort(buffer, encoderState, (short) 0);
+        encoder.writeUnsignedShort(buffer, encoderState, (short) 65535);
+
+        assertEquals(6, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.USHORT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), 0);
+        assertEquals(buffer.getByte(3), EncodingCodes.USHORT);
+        assertEquals(buffer.getByte(4), (byte) 255);
+        assertEquals(buffer.getByte(5), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedShortPrimitiveInt() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedShort(buffer, encoderState, 0);
+        encoder.writeUnsignedShort(buffer, encoderState, -1);
+        encoder.writeUnsignedShort(buffer, encoderState, 65535);
+
+        assertEquals(7, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.USHORT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), 0);
+        assertEquals(buffer.getByte(3), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(4), EncodingCodes.USHORT);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedIntegerObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(0));
+        encoder.writeUnsignedInteger(buffer, encoderState, (UnsignedInteger) null);
+        encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(255));
+        encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(-1));
+
+        assertEquals(9, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.UINT0);
+        assertEquals(buffer.getByte(1), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(2), EncodingCodes.SMALLUINT);
+        assertEquals(buffer.getByte(3), (byte) 255);
+        assertEquals(buffer.getByte(4), EncodingCodes.UINT);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+        assertEquals(buffer.getByte(7), (byte) 255);
+        assertEquals(buffer.getByte(8), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedIntegerPrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedInteger(buffer, encoderState, 0);
+        encoder.writeUnsignedInteger(buffer, encoderState, 255);
+        encoder.writeUnsignedInteger(buffer, encoderState, -1);
+
+        assertEquals(8, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.UINT0);
+        assertEquals(buffer.getByte(1), EncodingCodes.SMALLUINT);
+        assertEquals(buffer.getByte(2), (byte) 255);
+        assertEquals(buffer.getByte(3), EncodingCodes.UINT);
+        assertEquals(buffer.getByte(4), (byte) 255);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+        assertEquals(buffer.getByte(7), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedIntegerPrimitiveLong() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedInteger(buffer, encoderState, 0l);
+        encoder.writeUnsignedInteger(buffer, encoderState, 255l);
+        encoder.writeUnsignedInteger(buffer, encoderState, ((long) Integer.MAX_VALUE * 2) + 1);
+        encoder.writeUnsignedInteger(buffer, encoderState, -1l);
+
+        assertEquals(9, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.UINT0);
+        assertEquals(buffer.getByte(1), EncodingCodes.SMALLUINT);
+        assertEquals(buffer.getByte(2), (byte) 255);
+        assertEquals(buffer.getByte(3), EncodingCodes.UINT);
+        assertEquals(buffer.getByte(4), (byte) 255);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+        assertEquals(buffer.getByte(7), (byte) 255);
+        assertEquals(buffer.getByte(8), EncodingCodes.NULL);
+    }
+
+    @Test
+    public void testWriteUnsignedLongObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(0));
+        encoder.writeUnsignedLong(buffer, encoderState, (UnsignedLong) null);
+        encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(255));
+        encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(-1));
+
+        assertEquals(13, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.ULONG0);
+        assertEquals(buffer.getByte(1), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(2), EncodingCodes.SMALLULONG);
+        assertEquals(buffer.getByte(3), (byte) 255);
+        assertEquals(buffer.getByte(4), EncodingCodes.ULONG);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+        assertEquals(buffer.getByte(7), (byte) 255);
+        assertEquals(buffer.getByte(8), (byte) 255);
+        assertEquals(buffer.getByte(9), (byte) 255);
+        assertEquals(buffer.getByte(10), (byte) 255);
+        assertEquals(buffer.getByte(11), (byte) 255);
+        assertEquals(buffer.getByte(12), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedLongPrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, 0l);
+        encoder.writeUnsignedLong(buffer, encoderState, 255l);
+        encoder.writeUnsignedLong(buffer, encoderState, -1l  );
+
+        assertEquals(12, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.ULONG0);
+        assertEquals(buffer.getByte(1), EncodingCodes.SMALLULONG);
+        assertEquals(buffer.getByte(2), (byte) 255);
+        assertEquals(buffer.getByte(3), EncodingCodes.ULONG);
+        assertEquals(buffer.getByte(4), (byte) 255);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+        assertEquals(buffer.getByte(7), (byte) 255);
+        assertEquals(buffer.getByte(8), (byte) 255);
+        assertEquals(buffer.getByte(9), (byte) 255);
+        assertEquals(buffer.getByte(10), (byte) 255);
+        assertEquals(buffer.getByte(11), (byte) 255);
+    }
+
+    @Test
+    public void testWriteUnsignedLongPrimitiveByte() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, (byte) 0);
+        encoder.writeUnsignedLong(buffer, encoderState, (byte) 255);
+
+        assertEquals(3, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.ULONG0);
+        assertEquals(buffer.getByte(1), EncodingCodes.SMALLULONG);
+        assertEquals(buffer.getByte(2), (byte) 255);
+    }
+
+    @Test
+    public void testWriteByteObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeByte(buffer, encoderState, Byte.valueOf((byte) 0));
+        encoder.writeByte(buffer, encoderState, (Byte) null);
+        encoder.writeByte(buffer, encoderState, Byte.valueOf((byte) 255));
+
+        assertEquals(5, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.BYTE);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(3), EncodingCodes.BYTE);
+        assertEquals(buffer.getByte(4), (byte) 255);
+    }
+
+    @Test
+    public void testWriteBytePrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeByte(buffer, encoderState, (byte) 0);
+        encoder.writeByte(buffer, encoderState, (byte) 255);
+
+        assertEquals(4, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.BYTE);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), EncodingCodes.BYTE);
+        assertEquals(buffer.getByte(3), (byte) 255);
+    }
+
+    @Test
+    public void testWriteShortObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeShort(buffer, encoderState, Short.valueOf((short) 0));
+        encoder.writeShort(buffer, encoderState, (Short) null);
+        encoder.writeShort(buffer, encoderState, Short.valueOf((short) 65535));
+
+        assertEquals(7, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.SHORT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), 0);
+        assertEquals(buffer.getByte(3), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(4), EncodingCodes.SHORT);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+    }
+
+    @Test
+    public void testWriteShortPrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeShort(buffer, encoderState, (short) 0);
+        encoder.writeShort(buffer, encoderState, (short) 65535);
+
+        assertEquals(6, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.SHORT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), 0);
+        assertEquals(buffer.getByte(3), EncodingCodes.SHORT);
+        assertEquals(buffer.getByte(4), (byte) 255);
+        assertEquals(buffer.getByte(5), (byte) 255);
+    }
+
+    @Test
+    public void testWriteIntegerObject() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeInteger(buffer, encoderState, Integer.valueOf(0));
+        encoder.writeInteger(buffer, encoderState, (Integer) null);
+        encoder.writeInteger(buffer, encoderState, Integer.valueOf(Integer.MAX_VALUE));
+
+        assertEquals(8, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.SMALLINT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), EncodingCodes.NULL);
+        assertEquals(buffer.getByte(3), EncodingCodes.INT);
+        assertEquals(buffer.getByte(4), (byte) 127);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+        assertEquals(buffer.getByte(7), (byte) 255);
+    }
+
+    @Test
+    public void testWriteIntegerPrimitive() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeInteger(buffer, encoderState, 0);
+        encoder.writeInteger(buffer, encoderState, Integer.MAX_VALUE);
+
+        assertEquals(7, buffer.getReadableBytes());
+        assertEquals(buffer.getByte(0), EncodingCodes.SMALLINT);
+        assertEquals(buffer.getByte(1), 0);
+        assertEquals(buffer.getByte(2), EncodingCodes.INT);
+        assertEquals(buffer.getByte(3), (byte) 127);
+        assertEquals(buffer.getByte(4), (byte) 255);
+        assertEquals(buffer.getByte(5), (byte) 255);
+        assertEquals(buffer.getByte(6), (byte) 255);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/CodecToLegacyType.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/CodecToLegacyType.java
new file mode 100644
index 0000000..694dec6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/CodecToLegacyType.java
@@ -0,0 +1,792 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.legacy;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnClose;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinks;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinksOrMessages;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoMessages;
+import org.apache.qpid.protonj2.types.messaging.LifetimePolicy;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.messaging.Received;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+
+/**
+ * Set of methods for converting from Codec types to legacy proton-j types
+ * for use with the legacy codec when testing the codec.
+ */
+public abstract class CodecToLegacyType {
+
+    public static Object convertToLegacyType(Object newType) {
+
+        // Basic Types
+        if (newType instanceof UnsignedByte) {
+            return convertToLegacyType((UnsignedByte) newType);
+        } else if (newType instanceof UnsignedShort) {
+            return convertToLegacyType((UnsignedShort) newType);
+        } else if (newType instanceof UnsignedInteger) {
+            return convertToLegacyType((UnsignedInteger) newType);
+        } else if (newType instanceof UnsignedLong) {
+            return convertToLegacyType((UnsignedLong) newType);
+        } else if (newType instanceof Binary) {
+            return convertToLegacyType((Binary) newType);
+        } else if (newType instanceof Symbol) {
+            return convertToLegacyType((Symbol) newType);
+        } else if (newType instanceof Decimal32) {
+            return convertToLegacyType((Decimal32) newType);
+        } else if (newType instanceof Decimal64) {
+            return convertToLegacyType((Decimal64) newType);
+        } else if (newType instanceof Decimal128) {
+            return convertToLegacyType((Decimal128) newType);
+        }
+
+        // Arrays, Maps and Lists
+        if (newType instanceof Map) {
+            return convertToLegacyType((Map<?, ?>) newType);
+        } else if (newType instanceof List) {
+            return convertToLegacyType((List<?>) newType);
+        }
+
+        // Enumerations
+        if (newType instanceof Role) {
+            return convertToLegacyType((Role) newType);
+        } else if (newType instanceof SenderSettleMode) {
+            return convertToLegacyType((SenderSettleMode) newType);
+        } else if (newType instanceof ReceiverSettleMode) {
+            return convertToLegacyType((ReceiverSettleMode) newType);
+        } else if (newType instanceof TerminusDurability) {
+            return convertToLegacyType((TerminusDurability) newType);
+        } else if (newType instanceof TerminusExpiryPolicy) {
+            return convertToLegacyType((TerminusExpiryPolicy) newType);
+        }
+
+        // Messaging Types
+        if (newType instanceof Outcome) {
+            return convertToLegacyType((Outcome) newType);
+        } else if (newType instanceof DeliveryState) {
+            return convertToLegacyType((DeliveryState) newType);
+        } else if (newType instanceof Source) {
+            return convertToLegacyType((Source) newType);
+        } else if (newType instanceof Target) {
+            return convertToLegacyType((Target) newType);
+        } else if (newType instanceof Coordinator) {
+            return convertToLegacyType((Coordinator) newType);
+        } else if (newType instanceof LifetimePolicy) {
+            return convertToLegacyType((LifetimePolicy) newType);
+        }
+
+        // TODO - Other types as needed including transaction types
+        // Transaction Types
+
+        // Transport Types
+        if (newType instanceof Open) {
+            return convertToLegacyType((Open) newType);
+        } else if (newType instanceof Close) {
+            return convertToLegacyType((Close) newType);
+        } else if (newType instanceof Begin) {
+            return convertToLegacyType((Begin) newType);
+        } else if (newType instanceof End) {
+            return convertToLegacyType((End) newType);
+        } else if (newType instanceof Attach) {
+            return convertToLegacyType((Attach) newType);
+        } else if (newType instanceof Detach) {
+            return convertToLegacyType((Detach) newType);
+        } else if (newType instanceof ErrorCondition) {
+            return convertToLegacyType((ErrorCondition) newType);
+        }
+
+        // Security Types
+
+        return newType;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param open
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Open convertToLegacyType(Open open) {
+        org.apache.qpid.proton.amqp.transport.Open legacyOpen = new org.apache.qpid.proton.amqp.transport.Open();
+
+        legacyOpen.setContainerId(open.getContainerId());
+        legacyOpen.setHostname(open.getHostname());
+        if (open.hasMaxFrameSize()) {
+            legacyOpen.setMaxFrameSize(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(open.getMaxFrameSize()));
+        }
+        if (open.hasChannelMax()) {
+            legacyOpen.setChannelMax(org.apache.qpid.proton.amqp.UnsignedShort.valueOf((short) open.getChannelMax()));
+        }
+        if (open.hasIdleTimeout()) {
+            legacyOpen.setIdleTimeOut(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(open.getIdleTimeout()));
+        }
+        if (open.getOutgoingLocales() != null) {
+            legacyOpen.setOutgoingLocales(convertToLegacyType(open.getOutgoingLocales()));
+        }
+        if (open.getIncomingLocales() != null) {
+            legacyOpen.setIncomingLocales(convertToLegacyType(open.getIncomingLocales()));
+        }
+        if (open.getOfferedCapabilities() != null) {
+            legacyOpen.setOfferedCapabilities(convertToLegacyType(open.getOfferedCapabilities()));
+        }
+        if (open.getDesiredCapabilities() != null) {
+            legacyOpen.setDesiredCapabilities(convertToLegacyType(open.getDesiredCapabilities()));
+        }
+        if (open.getProperties() != null) {
+            legacyOpen.setProperties(convertToLegacyType(open.getProperties()));
+        }
+
+        return legacyOpen;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param close
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Close convertToLegacyType(Close close) {
+        org.apache.qpid.proton.amqp.transport.Close legacyClose = new org.apache.qpid.proton.amqp.transport.Close();
+
+        if (close.getError() != null) {
+            legacyClose.setError(convertToLegacyType(close.getError()));
+        }
+
+        return legacyClose;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param begin
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Begin convertToLegacyType(Begin begin) {
+        org.apache.qpid.proton.amqp.transport.Begin legacyBegin = new org.apache.qpid.proton.amqp.transport.Begin();
+
+        if (begin.hasHandleMax()) {
+            legacyBegin.setHandleMax(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(begin.getHandleMax()));
+        }
+        if (begin.hasIncomingWindow()) {
+            legacyBegin.setIncomingWindow(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(begin.getIncomingWindow()));
+        }
+        if (begin.hasNextOutgoingId()) {
+            legacyBegin.setNextOutgoingId(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(begin.getNextOutgoingId()));
+        }
+        if (begin.hasOutgoingWindow()) {
+            legacyBegin.setOutgoingWindow(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(begin.getOutgoingWindow()));
+        }
+        if (begin.hasRemoteChannel()) {
+            legacyBegin.setRemoteChannel(org.apache.qpid.proton.amqp.UnsignedShort.valueOf((short) begin.getRemoteChannel()));
+        }
+        if (begin.hasOfferedCapabilites()) {
+            legacyBegin.setOfferedCapabilities(convertToLegacyType(begin.getOfferedCapabilities()));
+        }
+        if (begin.hasDesiredCapabilites()) {
+            legacyBegin.setDesiredCapabilities(convertToLegacyType(begin.getDesiredCapabilities()));
+        }
+        if (begin.hasProperties()) {
+            legacyBegin.setProperties(convertToLegacyType(begin.getProperties()));
+        }
+
+        return legacyBegin;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param end
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.End convertToLegacyType(End end) {
+        org.apache.qpid.proton.amqp.transport.End legacyEnd = new org.apache.qpid.proton.amqp.transport.End();
+
+        if (end.getError() != null) {
+            legacyEnd.setError(convertToLegacyType(end.getError()));
+        }
+
+        return legacyEnd;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param attach
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Attach convertToLegacyType(Attach attach) {
+        org.apache.qpid.proton.amqp.transport.Attach legacyAttach = new org.apache.qpid.proton.amqp.transport.Attach();
+
+        if (attach.hasName()) {
+            legacyAttach.setName(attach.getName());
+        }
+        if (attach.hasHandle()) {
+            legacyAttach.setHandle(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(attach.getHandle()));
+        }
+        if (attach.hasRole()) {
+            legacyAttach.setRole(convertToLegacyType(attach.getRole()));
+        }
+        if (attach.hasSenderSettleMode()) {
+            legacyAttach.setSndSettleMode(convertToLegacyType(attach.getSenderSettleMode()));
+        }
+        if (attach.hasReceiverSettleMode()) {
+            legacyAttach.setRcvSettleMode(convertToLegacyType(attach.getReceiverSettleMode()));
+        }
+        if (attach.hasIncompleteUnsettled()) {
+            legacyAttach.setIncompleteUnsettled(attach.getIncompleteUnsettled());
+        }
+        if (attach.hasOfferedCapabilites()) {
+            legacyAttach.setOfferedCapabilities(convertToLegacyType(attach.getOfferedCapabilities()));
+        }
+        if (attach.hasDesiredCapabilites()) {
+            legacyAttach.setDesiredCapabilities(convertToLegacyType(attach.getDesiredCapabilities()));
+        }
+        if (attach.hasProperties()) {
+            legacyAttach.setProperties(convertToLegacyType(attach.getProperties()));
+        }
+        if (attach.hasInitialDeliveryCount()) {
+            legacyAttach.setInitialDeliveryCount(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(attach.getInitialDeliveryCount()));
+        }
+        if (attach.hasMaxMessageSize()) {
+            legacyAttach.setMaxMessageSize(convertToLegacyType(attach.getMaxMessageSize()));
+        }
+        if (attach.hasSource()) {
+            legacyAttach.setSource(convertToLegacyType(attach.getSource()));
+        }
+        if (attach.hasTarget()) {
+            Terminus instance = attach.getTarget();
+            legacyAttach.setTarget((org.apache.qpid.proton.amqp.transport.Target) convertToLegacyType(instance));
+        }
+        if (attach.hasUnsettled()) {
+            legacyAttach.setUnsettled(convertToLegacyType(attach.getUnsettled()));
+        }
+
+        return legacyAttach;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param detach
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Detach convertToLegacyType(Detach detach) {
+        org.apache.qpid.proton.amqp.transport.Detach legacyDetach = new org.apache.qpid.proton.amqp.transport.Detach();
+
+        if (detach.hasError()) {
+            legacyDetach.setError(convertToLegacyType(detach.getError()));
+        }
+        if (detach.hasClosed()) {
+            legacyDetach.setClosed(detach.getClosed());
+        }
+        if (detach.hasHandle()) {
+            legacyDetach.setHandle(org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(detach.getHandle()));
+        }
+
+        return legacyDetach;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param source
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Source convertToLegacyType(Source source) {
+        org.apache.qpid.proton.amqp.messaging.Source legacySource = new org.apache.qpid.proton.amqp.messaging.Source();
+
+        if (source.getAddress() != null) {
+            legacySource.setAddress(source.getAddress());
+        }
+        if (source.getDurable() != null) {
+            legacySource.setDurable(convertToLegacyType(source.getDurable()));
+        }
+        if (source.getExpiryPolicy() != null) {
+            legacySource.setExpiryPolicy(convertToLegacyType(source.getExpiryPolicy()));
+        }
+        if (source.getTimeout() != null) {
+            legacySource.setTimeout(convertToLegacyType(source.getTimeout()));
+        }
+        legacySource.setDynamic(source.isDynamic());
+        if (source.getDynamicNodeProperties() != null) {
+            legacySource.setDynamicNodeProperties(convertToLegacyType(source.getDynamicNodeProperties()));
+        }
+        if (source.getDistributionMode() != null) {
+            legacySource.setDistributionMode(convertToLegacyType(source.getDistributionMode()));
+        }
+        if (source.getFilter() != null) {
+            legacySource.setFilter(convertToLegacyType(source.getFilter()));
+        }
+        if (source.getDefaultOutcome() != null) {
+            legacySource.setDefaultOutcome(convertToLegacyType(source.getDefaultOutcome()));
+        }
+        if (source.getOutcomes() != null) {
+            legacySource.setOutcomes(convertToLegacyType(source.getOutcomes()));
+        }
+        if (source.getCapabilities() != null) {
+            legacySource.setCapabilities(convertToLegacyType(source.getCapabilities()));
+        }
+
+        return legacySource;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param target
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.messaging.Target convertToLegacyType(Target target) {
+        org.apache.qpid.proton.amqp.messaging.Target legacyTarget = new org.apache.qpid.proton.amqp.messaging.Target();
+
+        if (target.getAddress() != null) {
+            legacyTarget.setAddress(target.getAddress());
+        }
+        if (target.getDurable() != null) {
+            legacyTarget.setDurable(convertToLegacyType(target.getDurable()));
+        }
+        if (target.getExpiryPolicy() != null) {
+            legacyTarget.setExpiryPolicy(convertToLegacyType(target.getExpiryPolicy()));
+        }
+        if (target.getTimeout() != null) {
+            legacyTarget.setTimeout(convertToLegacyType(target.getTimeout()));
+        }
+        target.setDynamic(target.isDynamic());
+        if (target.getDynamicNodeProperties() != null) {
+            legacyTarget.setDynamicNodeProperties(convertToLegacyType(target.getDynamicNodeProperties()));
+        }
+        if (target.getCapabilities() != null) {
+            legacyTarget.setCapabilities(convertToLegacyType(target.getCapabilities()));
+        }
+
+        return legacyTarget;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param coordinator
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transaction.Coordinator convertToLegacyType(Coordinator coordinator) {
+        org.apache.qpid.proton.amqp.transaction.Coordinator legacyCoordinator =
+            new org.apache.qpid.proton.amqp.transaction.Coordinator();
+
+        if (coordinator.getCapabilities() != null) {
+            legacyCoordinator.setCapabilities(convertToLegacyType(coordinator.getCapabilities()));
+        }
+
+        return legacyCoordinator;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param map
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static Map<?, ?> convertToLegacyType(Map<?, ?> map) {
+        Map<Object, Object> legacySafeMap = new LinkedHashMap<>();
+
+        for (Entry<?, ?> entry : map.entrySet()) {
+            legacySafeMap.put(convertToLegacyType(entry.getKey()), convertToLegacyType(entry.getValue()));
+        }
+
+        return legacySafeMap;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param list
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static List<?> convertToLegacyType(List<?> list) {
+        List<Object> legacySafeList = new ArrayList<>();
+
+        for (Object entry : list) {
+            legacySafeList.add(convertToLegacyType(entry));
+        }
+
+        return legacySafeList;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param symbols
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.Symbol[] convertToLegacyType(Symbol[] symbols) {
+        org.apache.qpid.proton.amqp.Symbol[] legacySymbols = new org.apache.qpid.proton.amqp.Symbol[symbols.length];
+
+        for (int i = 0; i < symbols.length; ++i) {
+            legacySymbols[i] = org.apache.qpid.proton.amqp.Symbol.valueOf(symbols[i].toString());
+        }
+
+        return legacySymbols;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param binary
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.Binary convertToLegacyType(Binary binary) {
+        byte[] copy = new byte[binary.getLength()];
+        System.arraycopy(binary.getArray(), binary.getArrayOffset(), copy, 0, copy.length);
+        return new org.apache.qpid.proton.amqp.Binary(copy);
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param symbol
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.Symbol convertToLegacyType(Symbol symbol) {
+        return org.apache.qpid.proton.amqp.Symbol.valueOf(symbol.toString());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param ubyte
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.UnsignedByte convertToLegacyType(UnsignedByte ubyte) {
+        return org.apache.qpid.proton.amqp.UnsignedByte.valueOf(ubyte.byteValue());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param ushort
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.UnsignedShort convertToLegacyType(UnsignedShort ushort) {
+        return org.apache.qpid.proton.amqp.UnsignedShort.valueOf(ushort.shortValue());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param uint
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.UnsignedInteger convertToLegacyType(UnsignedInteger uint) {
+        return org.apache.qpid.proton.amqp.UnsignedInteger.valueOf(uint.intValue());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param ulong
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.UnsignedLong convertToLegacyType(UnsignedLong ulong) {
+        return org.apache.qpid.proton.amqp.UnsignedLong.valueOf(ulong.longValue());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param decimal32
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.Decimal32 convertToLegacyType(Decimal32 decimal32) {
+        return new org.apache.qpid.proton.amqp.Decimal32(decimal32.intValue());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param decimal64
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.Decimal64 convertToLegacyType(Decimal64 decimal64) {
+        return new org.apache.qpid.proton.amqp.Decimal64(decimal64.longValue());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param decimal128
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.Decimal128 convertToLegacyType(Decimal128 decimal128) {
+        return new org.apache.qpid.proton.amqp.Decimal128(decimal128.asBytes());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param terminusDurability
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.messaging.TerminusDurability convertToLegacyType(TerminusDurability terminusDurability) {
+        return org.apache.qpid.proton.amqp.messaging.TerminusDurability.valueOf(terminusDurability.name());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param terminusExpiryPolicy
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy convertToLegacyType(TerminusExpiryPolicy terminusExpiryPolicy) {
+        return org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy.valueOf(terminusExpiryPolicy.name());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param role
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.Role convertToLegacyType(Role role) {
+        return org.apache.qpid.proton.amqp.transport.Role.valueOf(role.name());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param senderSettleMode
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.SenderSettleMode convertToLegacyType(SenderSettleMode senderSettleMode) {
+        return org.apache.qpid.proton.amqp.transport.SenderSettleMode.valueOf(senderSettleMode.name());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param receiverSettleMode
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.ReceiverSettleMode convertToLegacyType(ReceiverSettleMode receiverSettleMode) {
+        return org.apache.qpid.proton.amqp.transport.ReceiverSettleMode.valueOf(receiverSettleMode.name());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param errorCondition
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.ErrorCondition convertToLegacyType(ErrorCondition errorCondition) {
+        org.apache.qpid.proton.amqp.transport.ErrorCondition legacyCondition = new org.apache.qpid.proton.amqp.transport.ErrorCondition();
+
+        if (errorCondition.getCondition() != null) {
+            legacyCondition.setCondition(convertToLegacyType(errorCondition.getCondition()));
+        }
+        if (errorCondition.getDescription() != null) {
+            legacyCondition.setDescription(errorCondition.getDescription());
+        }
+        if (errorCondition.getInfo() != null) {
+            legacyCondition.setInfo(convertToLegacyType(errorCondition.getInfo()));
+        }
+
+        return legacyCondition;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param state
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.transport.DeliveryState convertToLegacyType(DeliveryState state) {
+        if (state instanceof Accepted) {
+            return org.apache.qpid.proton.amqp.messaging.Accepted.getInstance();
+        } else if (state instanceof Rejected) {
+            org.apache.qpid.proton.amqp.messaging.Rejected rejected = new org.apache.qpid.proton.amqp.messaging.Rejected();
+            rejected.setError(convertToLegacyType(((Rejected) state).getError()));
+            return rejected;
+        } else if (state instanceof Released) {
+            return org.apache.qpid.proton.amqp.messaging.Released.getInstance();
+        } else if (state instanceof Modified) {
+            org.apache.qpid.proton.amqp.messaging.Modified modified = new org.apache.qpid.proton.amqp.messaging.Modified();
+            modified.setDeliveryFailed(((Modified) state).isDeliveryFailed());
+            modified.setMessageAnnotations(convertToLegacyType(((Modified) state).getMessageAnnotations()));
+            modified.setUndeliverableHere(((Modified) state).isUndeliverableHere());
+            return modified;
+        } else if (state instanceof Received) {
+            org.apache.qpid.proton.amqp.messaging.Received received = new org.apache.qpid.proton.amqp.messaging.Received();
+            received.setSectionOffset(convertToLegacyType(((Received) state).getSectionOffset()));
+            received.setSectionNumber(convertToLegacyType(((Received) state).getSectionNumber()));
+            return received;
+        } else if (state instanceof Declared) {
+            org.apache.qpid.proton.amqp.transaction.Declared declared = new org.apache.qpid.proton.amqp.transaction.Declared();
+            declared.setTxnId(convertToLegacyType(((Declared) state).getTxnId()));
+            return declared;
+        } else if (state instanceof TransactionalState) {
+            org.apache.qpid.proton.amqp.transaction.TransactionalState txState = new org.apache.qpid.proton.amqp.transaction.TransactionalState();
+            txState.setOutcome(convertToLegacyType(((TransactionalState) state).getOutcome()));
+            txState.setTxnId(convertToLegacyType(((TransactionalState) state).getTxnId()));
+            return txState;
+        }
+
+        return null;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param outcome
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.messaging.Outcome convertToLegacyType(Outcome outcome) {
+        if (outcome instanceof Accepted) {
+            return org.apache.qpid.proton.amqp.messaging.Accepted.getInstance();
+        } else if (outcome instanceof Rejected) {
+            org.apache.qpid.proton.amqp.messaging.Rejected rejected = new org.apache.qpid.proton.amqp.messaging.Rejected();
+            rejected.setError(convertToLegacyType(((Rejected) outcome).getError()));
+            return rejected;
+        } else if (outcome instanceof Released) {
+            return org.apache.qpid.proton.amqp.messaging.Released.getInstance();
+        } else if (outcome instanceof Modified) {
+            org.apache.qpid.proton.amqp.messaging.Modified modified = new org.apache.qpid.proton.amqp.messaging.Modified();
+            modified.setDeliveryFailed(((Modified) outcome).isDeliveryFailed());
+            modified.setMessageAnnotations(convertToLegacyType(((Modified) outcome).getMessageAnnotations()));
+            modified.setUndeliverableHere(((Modified) outcome).isUndeliverableHere());
+            return modified;
+        }
+
+        return null;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param policy
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static org.apache.qpid.proton.amqp.messaging.LifetimePolicy convertToLegacyType(LifetimePolicy policy) {
+        org.apache.qpid.proton.amqp.messaging.LifetimePolicy legacyPolicy = null;
+
+        if (policy instanceof DeleteOnClose) {
+            legacyPolicy = org.apache.qpid.proton.amqp.messaging.DeleteOnClose.getInstance();
+        } else if (policy instanceof DeleteOnNoLinks) {
+            legacyPolicy = org.apache.qpid.proton.amqp.messaging.DeleteOnNoLinks.getInstance();
+        } else if (policy instanceof DeleteOnNoLinksOrMessages) {
+            legacyPolicy = org.apache.qpid.proton.amqp.messaging.DeleteOnNoLinksOrMessages.getInstance();
+        } else if (policy instanceof DeleteOnNoMessages) {
+            legacyPolicy = org.apache.qpid.proton.amqp.messaging.DeleteOnNoMessages.getInstance();
+        }
+
+        return legacyPolicy;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/LegacyCodecAdapter.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/LegacyCodecAdapter.java
new file mode 100644
index 0000000..53b3879
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/LegacyCodecAdapter.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.legacy;
+
+import java.nio.ByteBuffer;
+
+import org.apache.qpid.proton.codec.AMQPDefinedTypes;
+import org.apache.qpid.proton.codec.DecoderImpl;
+import org.apache.qpid.proton.codec.EncoderImpl;
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+
+/**
+ * Adapter to allow using the legacy proton-j codec in tests for new proton library.
+ */
+public final class LegacyCodecAdapter {
+
+    private final DecoderImpl decoder = new DecoderImpl();
+    private final EncoderImpl encoder = new EncoderImpl(decoder);
+
+    /**
+     * Create a codec adapter instance.
+     */
+    public LegacyCodecAdapter() {
+        AMQPDefinedTypes.registerAllTypes(decoder, encoder);
+    }
+
+    /**
+     * Encode the given type using the legacy codec's Encoder implementation and then
+     * transfer the encoded bytes into a {@link ProtonBuffer} which can be used for
+     * decoding using the new codec.
+     *
+     * Usually this method should be passed a legacy type or other primitive value.
+     *
+     * @param value
+     *      The value to be encoded in a {@link ProtonBuffer}.
+     *
+     * @return a {@link ProtonBuffer} with the encoded bytes ready for reading.
+     */
+    public ProtonBuffer encodeUsingLegacyEncoder(Object value) {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
+
+        try {
+            encoder.setByteBuffer(byteBuffer);
+            encoder.writeObject(CodecToLegacyType.convertToLegacyType(value));
+            byteBuffer.flip();
+        } finally {
+            encoder.setByteBuffer((ByteBuffer) null);
+        }
+
+        buffer.writeBytes(byteBuffer);
+
+        return buffer;
+    }
+
+    /**
+     * Decode a proper legacy type from the given buffer and return it inside a type
+     * adapter that can be used for comparison.
+     *
+     * @param buffer
+     *      The buffer containing the encoded type.
+     *
+     * @return a {@link LegacyTypeAdapter} that can compare against the new version of the type.
+     */
+    public Object decodeLegacyType(ProtonBuffer buffer) {
+        ByteBuffer byteBuffer = buffer.toByteBuffer();
+        Object result = null;
+
+        try {
+            decoder.setByteBuffer(byteBuffer);
+            result = decoder.readObject();
+        } finally {
+            decoder.setBuffer(null);
+        }
+
+        return LegacyToCodecType.convertToCodecType(result);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/LegacyToCodecType.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/LegacyToCodecType.java
new file mode 100644
index 0000000..6d73cf4
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/legacy/LegacyToCodecType.java
@@ -0,0 +1,789 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.legacy;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnClose;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinks;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinksOrMessages;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoMessages;
+import org.apache.qpid.protonj2.types.messaging.LifetimePolicy;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Outcome;
+import org.apache.qpid.protonj2.types.messaging.Received;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+
+/**
+ * Converts from Legacy AMQP types to the new codec types to allow for cross testing
+ * using the legacy codec and then having new types to compare results.
+ */
+public abstract class LegacyToCodecType {
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyType
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Object convertToCodecType(Object legacyType) {
+
+        // Basic Types
+        if (legacyType instanceof org.apache.qpid.proton.amqp.UnsignedByte) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.UnsignedByte) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.UnsignedShort) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.UnsignedShort) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.UnsignedInteger) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.UnsignedInteger) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.UnsignedLong) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.UnsignedLong) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.Binary) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.Binary) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.Symbol) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.Symbol) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.Decimal32) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.Decimal32) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.Decimal64) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.Decimal64) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.Decimal128) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.Decimal128) legacyType);
+        }
+
+        // Arrays, Maps and Lists
+        if (legacyType instanceof Map) {
+            return convertToCodecType((Map<?, ?>) legacyType);
+        } // TODO - Convert Lists with legacy types to new codec types.
+
+        // Enumerations
+        if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Role) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Role) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.SenderSettleMode) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.SenderSettleMode) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.ReceiverSettleMode) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.ReceiverSettleMode) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.messaging.TerminusDurability) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.messaging.TerminusDurability) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy) legacyType);
+        }
+
+        // Messaging Types
+        if (legacyType instanceof org.apache.qpid.proton.amqp.messaging.Outcome) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.messaging.Outcome) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.messaging.Source) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.messaging.Source) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.messaging.Target) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.messaging.Target) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.messaging.LifetimePolicy) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.messaging.LifetimePolicy) legacyType);
+        }
+
+        // Transaction Types
+        // TODO - Transaction types converted to new codec types
+
+        // Transport Types
+        if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Open) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Open) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Close) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Close) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Begin) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Begin) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.End) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.End) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Attach) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Attach) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Detach) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Detach) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.ErrorCondition) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.ErrorCondition) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.DeliveryState) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.DeliveryState) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Source) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Source) legacyType);
+        } else if (legacyType instanceof org.apache.qpid.proton.amqp.transport.Target) {
+            return convertToCodecType((org.apache.qpid.proton.amqp.transport.Target) legacyType);
+        }
+
+        // Security Types
+
+        return legacyType;  // Pass through the type as we don't know how to convert it.
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyOpen
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    @SuppressWarnings("unchecked")
+    public static Open convertToCodecType(org.apache.qpid.proton.amqp.transport.Open legacyOpen) {
+        Open newOpen = new Open();
+
+        newOpen.setContainerId(legacyOpen.getContainerId());
+        newOpen.setHostname(legacyOpen.getHostname());
+        if (legacyOpen.getMaxFrameSize() != null) {
+            newOpen.setMaxFrameSize(legacyOpen.getMaxFrameSize().longValue());
+        }
+        if (legacyOpen.getChannelMax() != null) {
+            newOpen.setChannelMax(legacyOpen.getChannelMax().intValue());
+        }
+        if (legacyOpen.getIdleTimeOut() != null) {
+            newOpen.setIdleTimeout(legacyOpen.getIdleTimeOut().longValue());
+        }
+        if (legacyOpen.getOutgoingLocales() != null) {
+            newOpen.setOutgoingLocales(convertToCodecType(legacyOpen.getOutgoingLocales()));
+        }
+        if (legacyOpen.getIncomingLocales() != null) {
+            newOpen.setIncomingLocales(convertToCodecType(legacyOpen.getIncomingLocales()));
+        }
+        if (legacyOpen.getOfferedCapabilities() != null) {
+            newOpen.setOfferedCapabilities(convertToCodecType(legacyOpen.getOfferedCapabilities()));
+        }
+        if (legacyOpen.getDesiredCapabilities() != null) {
+            newOpen.setDesiredCapabilities(convertToCodecType(legacyOpen.getDesiredCapabilities()));
+        }
+        if (legacyOpen.getProperties() != null) {
+            newOpen.setProperties((Map<Symbol, Object>) convertToCodecType(legacyOpen.getProperties()));
+        }
+
+        return newOpen;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyClose
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Close convertToCodecType(org.apache.qpid.proton.amqp.transport.Close legacyClose) {
+        Close close = new Close();
+
+        if (legacyClose.getError() != null) {
+            close.setError(convertToCodecType(legacyClose.getError()));
+        }
+
+        return close;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyBegin
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    @SuppressWarnings("unchecked")
+    public static Begin convertToCodecType(org.apache.qpid.proton.amqp.transport.Begin legacyBegin) {
+        Begin begin = new Begin();
+
+        if (legacyBegin.getHandleMax() != null) {
+            begin.setHandleMax(legacyBegin.getHandleMax().longValue());
+        }
+        if (legacyBegin.getIncomingWindow() != null) {
+            begin.setIncomingWindow(legacyBegin.getIncomingWindow().longValue());
+        }
+        if (legacyBegin.getNextOutgoingId() != null) {
+            begin.setNextOutgoingId(legacyBegin.getNextOutgoingId().longValue());
+        }
+        if (legacyBegin.getOutgoingWindow() != null) {
+            begin.setOutgoingWindow(legacyBegin.getOutgoingWindow().longValue());
+        }
+        if (legacyBegin.getRemoteChannel() != null) {
+            begin.setRemoteChannel(legacyBegin.getRemoteChannel().intValue());
+        }
+        if (legacyBegin.getOfferedCapabilities() != null) {
+            begin.setOfferedCapabilities(convertToCodecType(legacyBegin.getOfferedCapabilities()));
+        }
+        if (legacyBegin.getDesiredCapabilities() != null) {
+            begin.setDesiredCapabilities(convertToCodecType(legacyBegin.getDesiredCapabilities()));
+        }
+        if (legacyBegin.getProperties() != null) {
+            begin.setProperties((Map<Symbol, Object>) convertToCodecType(legacyBegin.getProperties()));
+        }
+
+        return begin;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyEnd
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static End convertToCodecType(org.apache.qpid.proton.amqp.transport.End legacyEnd) {
+        End end = new End();
+
+        if (legacyEnd.getError() != null) {
+            end.setError(convertToCodecType(legacyEnd.getError()));
+        }
+
+        return end;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyAttach
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    @SuppressWarnings("unchecked")
+    public static Attach convertToCodecType(org.apache.qpid.proton.amqp.transport.Attach legacyAttach) {
+        Attach attach = new Attach();
+
+        if (legacyAttach.getName() != null) {
+            attach.setName(legacyAttach.getName());
+        }
+        if (legacyAttach.getHandle() != null) {
+            attach.setHandle(legacyAttach.getHandle().longValue());
+        }
+        if (legacyAttach.getRole() != null) {
+            attach.setRole(convertToCodecType(legacyAttach.getRole()));
+        }
+        if (legacyAttach.getSndSettleMode() != null) {
+            attach.setSenderSettleMode(convertToCodecType(legacyAttach.getSndSettleMode()));
+        }
+        if (legacyAttach.getRcvSettleMode() != null) {
+            attach.setReceiverSettleMode(convertToCodecType(legacyAttach.getRcvSettleMode()));
+        }
+        attach.setIncompleteUnsettled(legacyAttach.getIncompleteUnsettled());
+        if (legacyAttach.getOfferedCapabilities() != null) {
+            attach.setOfferedCapabilities(convertToCodecType(legacyAttach.getOfferedCapabilities()));
+        }
+        if (legacyAttach.getDesiredCapabilities() != null) {
+            attach.setDesiredCapabilities(convertToCodecType(legacyAttach.getDesiredCapabilities()));
+        }
+        if (legacyAttach.getProperties() != null) {
+            attach.setProperties((Map<Symbol, Object>) convertToCodecType(legacyAttach.getProperties()));
+        }
+        if (legacyAttach.getInitialDeliveryCount() != null) {
+            attach.setInitialDeliveryCount(legacyAttach.getInitialDeliveryCount().longValue());
+        }
+        if (legacyAttach.getMaxMessageSize() != null) {
+            attach.setMaxMessageSize(convertToCodecType(legacyAttach.getMaxMessageSize()));
+        }
+        if (legacyAttach.getSource() != null) {
+            attach.setSource(convertToCodecType(legacyAttach.getSource()));
+        }
+        if (legacyAttach.getTarget() != null) {
+            attach.setTarget(convertToCodecType(legacyAttach.getTarget()));
+        }
+        if (legacyAttach.getUnsettled() != null) {
+            attach.setUnsettled((Map<Binary, DeliveryState>) convertToCodecType(legacyAttach.getUnsettled()));
+        }
+
+        return attach;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyDetach
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Detach convertToCodecType(org.apache.qpid.proton.amqp.transport.Detach legacyDetach) {
+        Detach close = new Detach();
+
+        close.setClosed(legacyDetach.getClosed());
+        if (legacyDetach.getError() != null) {
+            close.setError(convertToCodecType(legacyDetach.getError()));
+        }
+        if (legacyDetach.getHandle() != null) {
+            close.setHandle(legacyDetach.getHandle().longValue());
+        }
+
+        return close;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacySource
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Source convertToCodecType(org.apache.qpid.proton.amqp.transport.Source legacySource) {
+        return convertToCodecType((org.apache.qpid.proton.amqp.messaging.Source) legacySource);
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyTarget
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Target convertToCodecType(org.apache.qpid.proton.amqp.transport.Target legacyTarget) {
+        return convertToCodecType((org.apache.qpid.proton.amqp.messaging.Target) legacyTarget);
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacySource
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    @SuppressWarnings("unchecked")
+    public static Source convertToCodecType(org.apache.qpid.proton.amqp.messaging.Source legacySource) {
+        Source source = new Source();
+
+        if (legacySource.getAddress() != null) {
+            source.setAddress(legacySource.getAddress());
+        }
+        if (legacySource.getDurable() != null) {
+            source.setDurable(convertToCodecType(legacySource.getDurable()));
+        }
+        if (legacySource.getExpiryPolicy() != null) {
+            source.setExpiryPolicy(convertToCodecType(legacySource.getExpiryPolicy()));
+        }
+        if (legacySource.getTimeout() != null) {
+            source.setTimeout(convertToCodecType(legacySource.getTimeout()));
+        }
+        source.setDynamic(legacySource.getDynamic());
+        if (legacySource.getDynamicNodeProperties() != null) {
+            source.setDynamicNodeProperties((Map<Symbol, Object>) convertToCodecType(legacySource.getDynamicNodeProperties()));
+        }
+        if (legacySource.getDistributionMode() != null) {
+            source.setDistributionMode(convertToCodecType(legacySource.getDistributionMode()));
+        }
+        if (legacySource.getFilter() != null) {
+            source.setFilter((Map<Symbol, Object>) convertToCodecType(legacySource.getFilter()));
+        }
+        if (legacySource.getDefaultOutcome() != null) {
+            source.setDefaultOutcome(convertToCodecType(legacySource.getDefaultOutcome()));
+        }
+        if (legacySource.getOutcomes() != null) {
+            source.setOutcomes(convertToCodecType(legacySource.getOutcomes()));
+        }
+        if (legacySource.getCapabilities() != null) {
+            source.setCapabilities(convertToCodecType(legacySource.getCapabilities()));
+        }
+
+        return source;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param legacyTarget
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    @SuppressWarnings("unchecked")
+    public static Target convertToCodecType(org.apache.qpid.proton.amqp.messaging.Target legacyTarget) {
+        Target target = new Target();
+
+        if (legacyTarget.getAddress() != null) {
+            target.setAddress(legacyTarget.getAddress());
+        }
+        if (legacyTarget.getDurable() != null) {
+            target.setDurable(convertToCodecType(legacyTarget.getDurable()));
+        }
+        if (legacyTarget.getExpiryPolicy() != null) {
+            target.setExpiryPolicy(convertToCodecType(legacyTarget.getExpiryPolicy()));
+        }
+        if (legacyTarget.getTimeout() != null) {
+            target.setTimeout(convertToCodecType(legacyTarget.getTimeout()));
+        }
+        legacyTarget.setDynamic(legacyTarget.getDynamic());
+        if (legacyTarget.getDynamicNodeProperties() != null) {
+            target.setDynamicNodeProperties((Map<Symbol, Object>) convertToCodecType(legacyTarget.getDynamicNodeProperties()));
+        }
+        if (legacyTarget.getCapabilities() != null) {
+            target.setCapabilities(convertToCodecType(legacyTarget.getCapabilities()));
+        }
+
+        return target;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param map
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Map<?, ?> convertToCodecType(Map<?, ?> map) {
+        Map<Object, Object> legacySafeMap = new LinkedHashMap<>();
+
+        for (Entry<?, ?> entry : map.entrySet()) {
+            legacySafeMap.put(convertToCodecType(entry.getKey()), convertToCodecType(entry.getValue()));
+        }
+
+        return legacySafeMap;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param symbols
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Symbol[] convertToCodecType(org.apache.qpid.proton.amqp.Symbol[] symbols) {
+        Symbol[] array = new Symbol[symbols.length];
+
+        for (int i = 0; i < symbols.length; ++i) {
+            array[i] = Symbol.valueOf(symbols[i].toString());
+        }
+
+        return array;
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param binary
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Binary convertToCodecType(org.apache.qpid.proton.amqp.Binary binary) {
+        byte[] copy = new byte[binary.getLength()];
+        System.arraycopy(binary.getArray(), binary.getArrayOffset(), copy, 0, copy.length);
+        return new Binary(copy);
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param symbol
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Symbol convertToCodecType(org.apache.qpid.proton.amqp.Symbol symbol) {
+        return Symbol.valueOf(symbol.toString());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param ubyte
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static UnsignedByte convertToCodecType(org.apache.qpid.proton.amqp.UnsignedByte ubyte) {
+        return UnsignedByte.valueOf(ubyte.byteValue());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param ushort
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static UnsignedShort convertToCodecType(org.apache.qpid.proton.amqp.UnsignedShort ushort) {
+        return UnsignedShort.valueOf(ushort.shortValue());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param uint
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static UnsignedInteger convertToCodecType(org.apache.qpid.proton.amqp.UnsignedInteger uint) {
+        return UnsignedInteger.valueOf(uint.intValue());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param ulong
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static UnsignedLong convertToCodecType(org.apache.qpid.proton.amqp.UnsignedLong ulong) {
+        return UnsignedLong.valueOf(ulong.longValue());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param decimal32
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Decimal32 convertToCodecType(org.apache.qpid.proton.amqp.Decimal32 decimal32) {
+        return new Decimal32(decimal32.intValue());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param decimal64
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Decimal64 convertToCodecType(org.apache.qpid.proton.amqp.Decimal64 decimal64) {
+        return new Decimal64(decimal64.longValue());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param decimal128
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Decimal128 convertToCodecType(org.apache.qpid.proton.amqp.Decimal128 decimal128) {
+        return new Decimal128(decimal128.asBytes());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param terminusDurability
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static TerminusDurability convertToCodecType(org.apache.qpid.proton.amqp.messaging.TerminusDurability terminusDurability) {
+        return TerminusDurability.valueOf(terminusDurability.name());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param terminusExpiryPolicy
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static TerminusExpiryPolicy convertToCodecType(org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy terminusExpiryPolicy) {
+        return TerminusExpiryPolicy.valueOf(terminusExpiryPolicy.name());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param role
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static Role convertToCodecType(org.apache.qpid.proton.amqp.transport.Role role) {
+        return Role.valueOf(role.name());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param senderSettleMode
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static SenderSettleMode convertToCodecType(org.apache.qpid.proton.amqp.transport.SenderSettleMode senderSettleMode) {
+        return SenderSettleMode.valueOf(senderSettleMode.name());
+    }
+
+    /**
+     * convert a legacy type to a new codec type for encoding or other operation that requires a new type.
+     *
+     * @param receiverSettleMode
+     *      The legacy type to be converted into a new codec equivalent type.
+     *
+     * @return the new codec version of the legacy type.
+     */
+    public static ReceiverSettleMode convertToCodecType(org.apache.qpid.proton.amqp.transport.ReceiverSettleMode receiverSettleMode) {
+        return ReceiverSettleMode.valueOf(receiverSettleMode.name());
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param errorCondition
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    @SuppressWarnings("unchecked")
+    public static ErrorCondition convertToCodecType(org.apache.qpid.proton.amqp.transport.ErrorCondition errorCondition) {
+        Symbol condition = null;
+        String description = null;
+        Map<Symbol, Object> info = null;
+
+        if (errorCondition.getCondition() != null) {
+            condition = convertToCodecType(errorCondition.getCondition());
+        }
+        if (errorCondition.getDescription() != null) {
+            description = errorCondition.getDescription();
+        }
+        if (errorCondition.getInfo() != null) {
+            info = (Map<Symbol, Object>) convertToCodecType(errorCondition.getInfo());
+        }
+
+        return new ErrorCondition(condition, description, info);
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param state
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    @SuppressWarnings("unchecked")
+    public static DeliveryState convertToCodecType(org.apache.qpid.proton.amqp.transport.DeliveryState state) {
+        if (state instanceof org.apache.qpid.proton.amqp.messaging.Accepted) {
+            return Accepted.getInstance();
+        } else if (state instanceof org.apache.qpid.proton.amqp.messaging.Rejected) {
+            Rejected rejected = new Rejected();
+            rejected.setError(convertToCodecType(((org.apache.qpid.proton.amqp.messaging.Rejected) state).getError()));
+            return rejected;
+        } else if (state instanceof org.apache.qpid.proton.amqp.messaging.Released) {
+            return Released.getInstance();
+        } else if (state instanceof org.apache.qpid.proton.amqp.messaging.Modified) {
+            Modified modified = new Modified();
+            modified.setDeliveryFailed(((org.apache.qpid.proton.amqp.messaging.Modified) state).getDeliveryFailed());
+            modified.setMessageAnnotations((Map<Symbol, Object>) convertToCodecType(((org.apache.qpid.proton.amqp.messaging.Modified) state).getMessageAnnotations()));
+            modified.setUndeliverableHere(((org.apache.qpid.proton.amqp.messaging.Modified) state).getUndeliverableHere());
+            return modified;
+        } else if (state instanceof org.apache.qpid.proton.amqp.messaging.Received) {
+            Received received = new Received();
+            received.setSectionOffset(convertToCodecType(((org.apache.qpid.proton.amqp.messaging.Received) state).getSectionOffset()));
+            received.setSectionNumber(convertToCodecType(((org.apache.qpid.proton.amqp.messaging.Received) state).getSectionNumber()));
+            return received;
+        } else if (state instanceof org.apache.qpid.proton.amqp.transaction.Declared) {
+            Declared declared = new Declared();
+            declared.setTxnId(convertToCodecType(((org.apache.qpid.proton.amqp.transaction.Declared) state).getTxnId()));
+            return declared;
+        } else if (state instanceof org.apache.qpid.proton.amqp.transaction.TransactionalState) {
+            TransactionalState txState = new TransactionalState();
+            txState.setOutcome(convertToCodecType(((org.apache.qpid.proton.amqp.transaction.TransactionalState) state).getOutcome()));
+            txState.setTxnId(convertToCodecType(((org.apache.qpid.proton.amqp.transaction.TransactionalState) state).getTxnId()));
+            return txState;
+        }
+
+        return null;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param outcome
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    @SuppressWarnings("unchecked")
+    public static Outcome convertToCodecType(org.apache.qpid.proton.amqp.messaging.Outcome outcome) {
+        if (outcome instanceof org.apache.qpid.proton.amqp.messaging.Accepted) {
+            return Accepted.getInstance();
+        } else if (outcome instanceof org.apache.qpid.proton.amqp.messaging.Rejected) {
+            Rejected rejected = new Rejected();
+            rejected.setError(convertToCodecType(((org.apache.qpid.proton.amqp.messaging.Rejected) outcome).getError()));
+            return rejected;
+        } else if (outcome instanceof org.apache.qpid.proton.amqp.messaging.Released) {
+            return Released.getInstance();
+        } else if (outcome instanceof org.apache.qpid.proton.amqp.messaging.Modified) {
+            Modified modified = new Modified();
+            modified.setDeliveryFailed(((org.apache.qpid.proton.amqp.messaging.Modified) outcome).getDeliveryFailed());
+            modified.setMessageAnnotations((Map<Symbol, Object>) convertToCodecType(((org.apache.qpid.proton.amqp.messaging.Modified) outcome).getMessageAnnotations()));
+            modified.setUndeliverableHere(((org.apache.qpid.proton.amqp.messaging.Modified) outcome).getUndeliverableHere());
+            return modified;
+        }
+
+        return null;
+    }
+
+    /**
+     * convert a new Codec type to a legacy type for encoding or other operation that requires a legacy type.
+     *
+     * @param policy
+     *      The new codec type to be converted to the legacy codec version
+     *
+     * @return the legacy version of the new type.
+     */
+    public static LifetimePolicy convertToCodecType(org.apache.qpid.proton.amqp.messaging.LifetimePolicy policy) {
+        LifetimePolicy legacyPolicy = null;
+
+        if (policy instanceof org.apache.qpid.proton.amqp.messaging.DeleteOnClose) {
+            legacyPolicy = DeleteOnClose.getInstance();
+        } else if (policy instanceof org.apache.qpid.proton.amqp.messaging.DeleteOnNoLinks) {
+            legacyPolicy = DeleteOnNoLinks.getInstance();
+        } else if (policy instanceof org.apache.qpid.proton.amqp.messaging.DeleteOnNoLinksOrMessages) {
+            legacyPolicy = DeleteOnNoLinksOrMessages.getInstance();
+        } else if (policy instanceof org.apache.qpid.proton.amqp.messaging.DeleteOnNoMessages) {
+            legacyPolicy = DeleteOnNoMessages.getInstance();
+        }
+
+        return legacyPolicy;
+    }
+
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AcceptedTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AcceptedTypeCodecTest.java
new file mode 100644
index 0000000..3feb77b
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AcceptedTypeCodecTest.java
@@ -0,0 +1,380 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AcceptedTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.AcceptedTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of Accepted types.
+ */
+public class AcceptedTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Accepted.class, new AcceptedTypeDecoder().getTypeClass());
+        assertEquals(Accepted.class, new AcceptedTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Accepted.DESCRIPTOR_CODE, new AcceptedTypeDecoder().getDescriptorCode());
+        assertEquals(Accepted.DESCRIPTOR_CODE, new AcceptedTypeEncoder().getDescriptorCode());
+        assertEquals(Accepted.DESCRIPTOR_SYMBOL, new AcceptedTypeDecoder().getDescriptorSymbol());
+        assertEquals(Accepted.DESCRIPTOR_SYMBOL, new AcceptedTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeAccepted() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Accepted value = Accepted.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Accepted);
+
+        Accepted decoded = (Accepted) result;
+
+        assertEquals(value, decoded);
+    }
+
+    @Test
+    public void testDecodeAcceptedFromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Accepted value = Accepted.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Accepted);
+
+        Accepted decoded = (Accepted) result;
+
+        assertEquals(value, decoded);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithList8() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        Accepted value = Accepted.getInstance();
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Accepted);
+
+        Accepted decoded = (Accepted) result;
+
+        assertEquals(value, decoded);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithList8FromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        Accepted value = Accepted.getInstance();
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Accepted);
+
+        Accepted decoded = (Accepted) result;
+
+        assertEquals(value, decoded);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithList32() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        Accepted value = Accepted.getInstance();
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Accepted);
+
+        Accepted decoded = (Accepted) result;
+
+        assertEquals(value, decoded);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithList32FromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        Accepted value = Accepted.getInstance();
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Accepted);
+
+        Accepted decoded = (Accepted) result;
+
+        assertEquals(value, decoded);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithInvalidMap32Type() throws IOException {
+        doTestDecodeAcceptedWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithInvalidMap8Type() throws IOException {
+        doTestDecodeAcceptedWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeAcceptedWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeAcceptedWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeAcceptedWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeAcceptedWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        try {
+            if (fromStream) {
+                streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoder.readObject(buffer, decoderState);
+            }
+            fail("Should not decode type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Accepted.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Accepted.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Accepted.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, Accepted.getInstance());
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Accepted.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Accepted.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Accepted[] array = new Accepted[3];
+
+        array[0] = Accepted.getInstance();
+        array[1] = Accepted.getInstance();
+        array[2] = Accepted.getInstance();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Accepted.class, result.getClass().getComponentType());
+
+        Accepted[] resultArray = (Accepted[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Accepted);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AmqpSequenceTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AmqpSequenceTypeCodecTest.java
new file mode 100644
index 0000000..96178fc
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AmqpSequenceTypeCodecTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AmqpSequenceTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.AmqpSequenceTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.AmqpSequence;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for decoder of the AmqpValue type.
+ */
+public class AmqpSequenceTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(AmqpSequence.class, new AmqpSequenceTypeDecoder().getTypeClass());
+        assertEquals(AmqpSequence.class, new AmqpSequenceTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(AmqpSequence.DESCRIPTOR_CODE, new AmqpSequenceTypeDecoder().getDescriptorCode());
+        assertEquals(AmqpSequence.DESCRIPTOR_CODE, new AmqpSequenceTypeEncoder().getDescriptorCode());
+        assertEquals(AmqpSequence.DESCRIPTOR_SYMBOL, new AmqpSequenceTypeDecoder().getDescriptorSymbol());
+        assertEquals(AmqpSequence.DESCRIPTOR_SYMBOL, new AmqpSequenceTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeAmqpValueString() throws IOException {
+        doTestDecodeAmqpValueString(false);
+    }
+
+    @Test
+    public void testDecodeAmqpValueStringFromStream() throws IOException {
+        doTestDecodeAmqpValueString(true);
+    }
+
+    private void doTestDecodeAmqpValueString(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<Object> list = new ArrayList<>();
+
+        list.add(UUID.randomUUID());
+        list.add("string");
+
+        AmqpSequence<Object> value = new AmqpSequence<>(list);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof AmqpSequence);
+
+        @SuppressWarnings("unchecked")
+        AmqpSequence<Object> decoded = (AmqpSequence<Object>) result;
+
+        assertEquals(value.getValue(), decoded.getValue());
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfAmqpSequence() throws IOException {
+        doTestEncodeDecodeArrayOfAmqpSequence(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfAmqpSequenceFromStream() throws IOException {
+        doTestEncodeDecodeArrayOfAmqpSequence(true);
+    }
+
+    private void doTestEncodeDecodeArrayOfAmqpSequence(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<Object> list = new ArrayList<>();
+
+        list.add("test-1");
+        list.add("test-2");
+
+        @SuppressWarnings("unchecked")
+        AmqpSequence<Object>[] array = new AmqpSequence[3];
+
+        array[0] = new AmqpSequence<>(list);
+        array[1] = new AmqpSequence<>(list);
+        array[2] = new AmqpSequence<>(list);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(AmqpSequence.class, result.getClass().getComponentType());
+
+        @SuppressWarnings("unchecked")
+        AmqpSequence<Object>[] resultArray = (AmqpSequence[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof AmqpSequence);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<Object> list = new ArrayList<>();
+        list.add("one");
+        list.add("two");
+        list.add("three");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new AmqpSequence<>(list));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(AmqpSequence.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(AmqpSequence.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(AmqpSequence.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(AmqpSequence.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(AmqpSequence.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(AmqpSequence.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    @SuppressWarnings("rawtypes")
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        AmqpSequence[] array = new AmqpSequence[3];
+
+        List<Object> list = new ArrayList<>();
+        list.add("1");
+        list.add("2");
+
+        array[0] = new AmqpSequence<>(new ArrayList<>());
+        array[1] = new AmqpSequence<>(list);
+        array[2] = new AmqpSequence<>(list);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(AmqpSequence.class, result.getClass().getComponentType());
+
+        AmqpSequence[] resultArray = (AmqpSequence[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof AmqpSequence);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AmqpValueTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AmqpValueTypeCodecTest.java
new file mode 100644
index 0000000..bf09b38
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/AmqpValueTypeCodecTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.AmqpValueTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.AmqpValueTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for decoder of the AmqpValue type.
+ */
+public class AmqpValueTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(AmqpValue.class, new AmqpValueTypeDecoder().getTypeClass());
+        assertEquals(AmqpValue.class, new AmqpValueTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(AmqpValue.DESCRIPTOR_CODE, new AmqpValueTypeDecoder().getDescriptorCode());
+        assertEquals(AmqpValue.DESCRIPTOR_CODE, new AmqpValueTypeEncoder().getDescriptorCode());
+        assertEquals(AmqpValue.DESCRIPTOR_SYMBOL, new AmqpValueTypeDecoder().getDescriptorSymbol());
+        assertEquals(AmqpValue.DESCRIPTOR_SYMBOL, new AmqpValueTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeAmqpValueString() throws IOException {
+        doTestDecodeAmqpValueSeries(1, new AmqpValue<>("test"), false);
+    }
+
+    @Test
+    public void testDecodeAmqpValueNull() throws IOException {
+        doTestDecodeAmqpValueSeries(1, new AmqpValue<>(null), false);
+    }
+
+    @Test
+    public void testDecodeAmqpValueUUID() throws IOException {
+        doTestDecodeAmqpValueSeries(1, new AmqpValue<>(UUID.randomUUID()), false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfAmqpValue() throws IOException {
+        doTestDecodeAmqpValueSeries(SMALL_SIZE, new AmqpValue<>("test"), false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfAmqpValue() throws IOException {
+        doTestDecodeAmqpValueSeries(LARGE_SIZE, new AmqpValue<>("test"), false);
+    }
+
+    @Test
+    public void testDecodeAmqpValueStringFromStream() throws IOException {
+        doTestDecodeAmqpValueSeries(1, new AmqpValue<>("test"), true);
+    }
+
+    @Test
+    public void testDecodeAmqpValueNullFromStream() throws IOException {
+        doTestDecodeAmqpValueSeries(1, new AmqpValue<>(null), true);
+    }
+
+    @Test
+    public void testDecodeAmqpValueUUIDFromStream() throws IOException {
+        doTestDecodeAmqpValueSeries(1, new AmqpValue<>(UUID.randomUUID()), true);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfAmqpValueFromStream() throws IOException {
+        doTestDecodeAmqpValueSeries(SMALL_SIZE, new AmqpValue<>("test"), true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfAmqpValueFromStream() throws IOException {
+        doTestDecodeAmqpValueSeries(LARGE_SIZE, new AmqpValue<>("test"), true);
+    }
+
+    private void doTestDecodeAmqpValueSeries(int size, AmqpValue<Object> value, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, value);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof AmqpValue);
+
+            @SuppressWarnings("unchecked")
+            AmqpValue<Object> decoded = (AmqpValue<Object>) result;
+
+            assertEquals(value.getValue(), decoded.getValue());
+        }
+    }
+
+    @Test
+    public void testDecodeAmqpValueWithEmptyValue() throws IOException {
+        doTestDecodeAmqpValueWithEmptyValue(false);
+    }
+
+    @Test
+    public void testDecodeAmqpValueWithEmptyValueFromStream() throws IOException {
+        doTestDecodeAmqpValueWithEmptyValue(true);
+    }
+
+    private void doTestDecodeAmqpValueWithEmptyValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, new AmqpValue<>(null));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof AmqpValue);
+
+        AmqpValue<?> decoded = (AmqpValue<?>) result;
+
+        assertNull(decoded.getValue());
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfAmqpValue() throws IOException {
+        doTestEncodeDecodeArrayOfAmqpValue(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfAmqpValueFromStream() throws IOException {
+        doTestEncodeDecodeArrayOfAmqpValue(true);
+    }
+
+    private void doTestEncodeDecodeArrayOfAmqpValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        @SuppressWarnings("unchecked")
+        AmqpValue<Object>[] array = new AmqpValue[3];
+
+        array[0] = new AmqpValue<>("1");
+        array[1] = new AmqpValue<>("2");
+        array[2] = new AmqpValue<>("3");
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(AmqpValue.class, result.getClass().getComponentType());
+
+        @SuppressWarnings("unchecked")
+        AmqpValue<String>[] resultArray = (AmqpValue[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof AmqpValue);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new AmqpValue<>("skipMe"));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(AmqpValue.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(AmqpValue.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        AmqpValue[] array = new AmqpValue[3];
+
+        array[0] = new AmqpValue<>("1");
+        array[1] = new AmqpValue<>("2");
+        array[2] = new AmqpValue<>("3");
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(AmqpValue.class, result.getClass().getComponentType());
+
+        AmqpValue[] resultArray = (AmqpValue[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof AmqpValue);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ApplicationPropertiesTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ApplicationPropertiesTypeCodecTest.java
new file mode 100644
index 0000000..ff1f7e9
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ApplicationPropertiesTypeCodecTest.java
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ApplicationPropertiesTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ApplicationPropertiesTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.ApplicationProperties;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+public class ApplicationPropertiesTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(ApplicationProperties.class, new ApplicationPropertiesTypeDecoder().getTypeClass());
+        assertEquals(ApplicationProperties.class, new ApplicationPropertiesTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(ApplicationProperties.DESCRIPTOR_CODE, new ApplicationPropertiesTypeDecoder().getDescriptorCode());
+        assertEquals(ApplicationProperties.DESCRIPTOR_CODE, new ApplicationPropertiesTypeEncoder().getDescriptorCode());
+        assertEquals(ApplicationProperties.DESCRIPTOR_SYMBOL, new ApplicationPropertiesTypeDecoder().getDescriptorSymbol());
+        assertEquals(ApplicationProperties.DESCRIPTOR_SYMBOL, new ApplicationPropertiesTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfApplicationProperties() throws IOException {
+        doTestDecodeHeaderSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfApplicationProperties() throws IOException {
+        doTestDecodeHeaderSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfApplicationPropertiesFromStream() throws IOException {
+        doTestDecodeHeaderSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfApplicationPropertiesFromStream() throws IOException {
+        doTestDecodeHeaderSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeHeaderSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<String, Object> propertiesMap = new LinkedHashMap<>();
+        ApplicationProperties properties = new ApplicationProperties(propertiesMap);
+
+        propertiesMap.put("key-1", "1");
+        propertiesMap.put("key-2", "2");
+        propertiesMap.put("key-3", "3");
+        propertiesMap.put("key-4", "4");
+        propertiesMap.put("key-5", "5");
+        propertiesMap.put("key-6", "6");
+        propertiesMap.put("key-7", "7");
+        propertiesMap.put("key-8", "8");
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final ApplicationProperties result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState, ApplicationProperties.class);
+            } else {
+                result = decoder.readObject(buffer, decoderState, ApplicationProperties.class);
+            }
+
+            assertNotNull(result);
+            assertEquals(8, result.getValue().size());
+            assertTrue(result.getValue().equals(propertiesMap));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeZeroSizedArrayOfApplicationProperties() throws IOException {
+        doTestEncodeDecodeZeroSizedArrayOfApplicationProperties(false);
+    }
+
+    @Test
+    public void testEncodeDecodeZeroSizedArrayOfApplicationPropertiesFromStream() throws IOException {
+        doTestEncodeDecodeZeroSizedArrayOfApplicationProperties(true);
+    }
+
+    private void doTestEncodeDecodeZeroSizedArrayOfApplicationProperties(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ApplicationProperties[] array = new ApplicationProperties[0];
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(ApplicationProperties.class, result.getClass().getComponentType());
+
+        ApplicationProperties[] resultArray = (ApplicationProperties[]) result;
+        assertEquals(0, resultArray.length);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfApplicationProperties() throws IOException {
+        testEncodeDecodeArrayOfApplicationProperties(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfApplicationPropertiesFromStream() throws IOException {
+        testEncodeDecodeArrayOfApplicationProperties(true);
+    }
+
+    private void testEncodeDecodeArrayOfApplicationProperties(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ApplicationProperties[] array = new ApplicationProperties[3];
+
+        array[0] = new ApplicationProperties(new HashMap<String, Object>());
+        array[1] = new ApplicationProperties(new HashMap<String, Object>());
+        array[2] = new ApplicationProperties(new HashMap<String, Object>());
+
+        array[0].getValue().put("key-1", "1");
+        array[1].getValue().put("key-1", "2");
+        array[2].getValue().put("key-1", "3");
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(ApplicationProperties.class, result.getClass().getComponentType());
+
+        ApplicationProperties[] resultArray = (ApplicationProperties[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof ApplicationProperties);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("one", 1);
+        map.put("two", Boolean.TRUE);
+        map.put("three", "test");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new ApplicationProperties(map));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(ApplicationProperties.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(ApplicationProperties.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValue() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(false);
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValueFromStream() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(true);
+    }
+
+    private void doTestEncodeDecodeMessageAnnotationsWithEmptyValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, new ApplicationProperties(null));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof ApplicationProperties);
+
+        ApplicationProperties readAnnotations = (ApplicationProperties) result;
+        assertNull(readAnnotations.getValue());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, true);
+    }
+
+    private void doTestSkipValueWithInvalidListType(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ApplicationProperties.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8){
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(ApplicationProperties.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(ApplicationProperties.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncoding() throws IOException {
+        doTestSkipValueWithNullMapEncoding(false);
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncodingFromStream() throws IOException {
+        doTestSkipValueWithNullMapEncoding(true);
+    }
+
+    private void doTestSkipValueWithNullMapEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ApplicationProperties.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(ApplicationProperties.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(ApplicationProperties.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ApplicationProperties[] array = new ApplicationProperties[3];
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("1", Boolean.TRUE);
+        map.put("2", Boolean.FALSE);
+
+        array[0] = new ApplicationProperties(new HashMap<>());
+        array[1] = new ApplicationProperties(map);
+        array[2] = new ApplicationProperties(map);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(ApplicationProperties.class, result.getClass().getComponentType());
+
+        ApplicationProperties[] resultArray = (ApplicationProperties[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof ApplicationProperties);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testReadTypeWithNullEncoding() throws IOException {
+        testReadTypeWithNullEncoding(false);
+    }
+
+    @Test
+    public void testReadTypeWithNullEncodingFromStream() throws IOException {
+        testReadTypeWithNullEncoding(true);
+    }
+
+    private void testReadTypeWithNullEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ApplicationProperties.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof ApplicationProperties);
+
+        ApplicationProperties decoded = (ApplicationProperties) result;
+        assertNull(decoded.getValue());
+    }
+
+    @Test
+    public void testReadTypeWithOverLargeEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ApplicationProperties.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.MAP32);
+        buffer.writeInt(Integer.MAX_VALUE);  // Size
+        buffer.writeInt(4);  // Count
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not decode type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DataTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DataTypeCodecTest.java
new file mode 100644
index 0000000..fd878ee
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DataTypeCodecTest.java
@@ -0,0 +1,532 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectOutputStream;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DataTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DataTypeEncoder;
+import org.apache.qpid.protonj2.codec.util.SimplePojo;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Timeout(20)
+public class DataTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Data.class, new DataTypeDecoder().getTypeClass());
+        assertEquals(Data.class, new DataTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Data.DESCRIPTOR_CODE, new DataTypeDecoder().getDescriptorCode());
+        assertEquals(Data.DESCRIPTOR_CODE, new DataTypeEncoder().getDescriptorCode());
+        assertEquals(Data.DESCRIPTOR_SYMBOL, new DataTypeDecoder().getDescriptorSymbol());
+        assertEquals(Data.DESCRIPTOR_SYMBOL, new DataTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeData() throws IOException {
+        doTestDecodeDataSeries(1, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfDatas() throws IOException {
+        doTestDecodeDataSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfDatas() throws IOException {
+        doTestDecodeDataSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeDataFromStream() throws IOException {
+        doTestDecodeDataSeries(1, true);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfDatasFromStream() throws IOException {
+        doTestDecodeDataSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfDatasFromStream() throws IOException {
+        doTestDecodeDataSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeDataSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Data data = new Data(new Binary(new byte[] { 1, 2, 3}));
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, data);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Data);
+
+            Data decoded = (Data) result;
+
+            assertArrayEquals(data.getValue(), decoded.getValue());
+        }
+    }
+
+    @Test
+    public void testDecodeDataWithPayloadInUpperBoundsOfSmallBinaryEncoding() throws IOException {
+        doTestDecodeDataWithPayloadInUpperBoundsOfSmallBinaryEncoding(false);
+    }
+
+    @Test
+    public void testDecodeDataWithPayloadInUpperBoundsOfSmallBinaryEncodingFromStream() throws IOException {
+        doTestDecodeDataWithPayloadInUpperBoundsOfSmallBinaryEncoding(true);
+    }
+
+    private void doTestDecodeDataWithPayloadInUpperBoundsOfSmallBinaryEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int SIZE = 240;
+
+        Data data = new Data(new Binary(new byte[SIZE]));
+        for (int i = 0; i < SIZE; ++i) {
+            data.getValue()[i] = (byte) i;
+        }
+
+        for (int i = 0; i < SIZE; ++i) {
+            data.getBinary().getArray()[i] = (byte) i;
+        }
+
+        encoder.writeObject(buffer, encoderState, data);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Data);
+
+        Data decoded = (Data) result;
+
+        assertEquals(data.getBinary(), decoded.getBinary());
+    }
+
+    @Test
+    public void testDecodeDataWithPayloadInVBIN32BinaryEncoding() throws IOException {
+        doTestDecodeDataWithPayloadInVBIN32BinaryEncoding(false);
+    }
+
+    @Test
+    public void testDecodeDataWithPayloadInVBIN32BinaryEncodingFromStream() throws IOException {
+        doTestDecodeDataWithPayloadInVBIN32BinaryEncoding(true);
+    }
+
+    private void doTestDecodeDataWithPayloadInVBIN32BinaryEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int SIZE = 65535;
+
+        Data data = new Data(new Binary(new byte[SIZE]));
+        for (int i = 0; i < SIZE; ++i) {
+            data.getValue()[i] = (byte) i;
+        }
+
+        for (int i = 0; i < SIZE; ++i) {
+            data.getBinary().getArray()[i] = (byte) i;
+        }
+
+        encoder.writeObject(buffer, encoderState, data);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Data);
+
+        Data decoded = (Data) result;
+
+        assertEquals(data.getBinary(), decoded.getBinary());
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfDataSections() throws IOException {
+        doTestEncodeDecodeArrayOfDataSections(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfDataSectionsFromStream() throws IOException {
+        doTestEncodeDecodeArrayOfDataSections(true);
+    }
+
+    private void doTestEncodeDecodeArrayOfDataSections(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Data[] dataArray = new Data[3];
+
+        dataArray[0] = new Data(new Binary(new byte[] { 1, 2, 3}));
+        dataArray[1] = new Data(new Binary(new byte[] { 4, 5, 6}));
+        dataArray[2] = new Data(new Binary(new byte[] { 7, 8, 9}));
+
+        encoder.writeObject(buffer, encoderState, dataArray);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Data.class, result.getClass().getComponentType());
+
+        Data[] resultArray = (Data[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Data);
+            assertEquals(dataArray[i].getBinary(), resultArray[i].getBinary());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new Data(new Binary(new byte[] { (byte) i })));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Data.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Data.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Data.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Data.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeSerializedTypeFromDataSection() throws IOException {
+        doTestDecodeSerializedTypeFromDataSection(false);
+    }
+
+    @Test
+    public void testDecodeSerializedTypeFromDataSectionFromStream() throws IOException {
+        doTestDecodeSerializedTypeFromDataSection(true);
+    }
+
+    private void doTestDecodeSerializedTypeFromDataSection(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SimplePojo expectedContent = new SimplePojo(UUID.randomUUID());
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(expectedContent);
+        oos.flush();
+        oos.close();
+        byte[] bytes = baos.toByteArray();
+
+        encoder.writeObject(buffer, encoderState, new Binary(bytes));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Binary);
+        Binary binary = (Binary) result;
+        assertEquals(bytes.length, binary.getLength());
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Data[] array = new Data[3];
+
+        Binary bytes1 = new Binary(new byte[] {0});
+        Binary bytes2 = new Binary(new byte[] {1});
+        Binary bytes3 = new Binary(new byte[] {2});
+
+        array[0] = new Data(bytes1);
+        array[1] = new Data(bytes2);
+        array[2] = new Data(bytes3);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Data.class, result.getClass().getComponentType());
+
+        Data[] resultArray = (Data[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Data);
+            assertArrayEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testReadTypeWithNullEncoding() throws IOException {
+        testReadTypeWithNullEncoding(false);
+    }
+
+    @Test
+    public void testReadTypeWithNullEncodingFromStream() throws IOException {
+        testReadTypeWithNullEncoding(true);
+    }
+
+    private void testReadTypeWithNullEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Data);
+
+        Data decoded = (Data) result;
+        assertNull(decoded.getBinary());
+    }
+
+    @Test
+    public void testReadTypeWithOverLargeEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Data.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(Integer.MAX_VALUE); // Not enough bytes in buffer for this
+        buffer.writeByte(0);
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not decode type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnCloseTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnCloseTypeCodecTest.java
new file mode 100644
index 0000000..ff12012
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnCloseTypeCodecTest.java
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnCloseTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnCloseTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnClose;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of DeleteOnClose types.
+ */
+public class DeleteOnCloseTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(DeleteOnClose.class, new DeleteOnCloseTypeDecoder().getTypeClass());
+        assertEquals(DeleteOnClose.class, new DeleteOnCloseTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(DeleteOnClose.DESCRIPTOR_CODE, new DeleteOnCloseTypeDecoder().getDescriptorCode());
+        assertEquals(DeleteOnClose.DESCRIPTOR_CODE, new DeleteOnCloseTypeEncoder().getDescriptorCode());
+        assertEquals(DeleteOnClose.DESCRIPTOR_SYMBOL, new DeleteOnCloseTypeDecoder().getDescriptorSymbol());
+        assertEquals(DeleteOnClose.DESCRIPTOR_SYMBOL, new DeleteOnCloseTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeDeleteOnClose() throws IOException {
+        doTestDecodeDeleteOnClose(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnCloseFromStream() throws IOException {
+        doTestDecodeDeleteOnClose(true);
+    }
+
+    private void doTestDecodeDeleteOnClose(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnClose value = DeleteOnClose.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnClose);
+    }
+
+    @Test
+    public void testDecodeDeleteOnCloseWithList8() throws IOException {
+        doTestDecodeDeleteOnCloseWithList8(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnCloseWithList8FromStream() throws IOException {
+        doTestDecodeDeleteOnCloseWithList8(true);
+    }
+
+    private void doTestDecodeDeleteOnCloseWithList8(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnClose.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnClose);
+    }
+
+    @Test
+    public void testDecodeDeleteOnCloseWithList32() throws IOException {
+        doTestDecodeDeleteOnCloseWithList32(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnCloseWithList32FromStream() throws IOException {
+        doTestDecodeDeleteOnCloseWithList32(true);
+    }
+
+    private void doTestDecodeDeleteOnCloseWithList32(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnClose.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnClose);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, DeleteOnClose.getInstance());
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(DeleteOnClose.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(DeleteOnClose.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnClose.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnClose.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(DeleteOnClose.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(DeleteOnClose.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnClose[] array = new DeleteOnClose[3];
+
+        array[0] = DeleteOnClose.getInstance();
+        array[1] = DeleteOnClose.getInstance();
+        array[2] = DeleteOnClose.getInstance();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(DeleteOnClose.class, result.getClass().getComponentType());
+
+        DeleteOnClose[] resultArray = (DeleteOnClose[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof DeleteOnClose);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoLinksOrMessagesCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoLinksOrMessagesCodecTest.java
new file mode 100644
index 0000000..d7349d6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoLinksOrMessagesCodecTest.java
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoLinksOrMessagesTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnNoLinksOrMessagesTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinksOrMessages;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of DeleteOnNoLinksOrMessages types.
+ */
+public class DeleteOnNoLinksOrMessagesCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(DeleteOnNoLinksOrMessages.class, new DeleteOnNoLinksOrMessagesTypeDecoder().getTypeClass());
+        assertEquals(DeleteOnNoLinksOrMessages.class, new DeleteOnNoLinksOrMessagesTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE, new DeleteOnNoLinksOrMessagesTypeDecoder().getDescriptorCode());
+        assertEquals(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE, new DeleteOnNoLinksOrMessagesTypeEncoder().getDescriptorCode());
+        assertEquals(DeleteOnNoLinksOrMessages.DESCRIPTOR_SYMBOL, new DeleteOnNoLinksOrMessagesTypeDecoder().getDescriptorSymbol());
+        assertEquals(DeleteOnNoLinksOrMessages.DESCRIPTOR_SYMBOL, new DeleteOnNoLinksOrMessagesTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksOrMessages() throws IOException {
+        doTestDecodeDeleteOnNoLinksOrMessages(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksOrMessagesFromStream() throws IOException {
+        doTestDecodeDeleteOnNoLinksOrMessages(true);
+    }
+
+    private void doTestDecodeDeleteOnNoLinksOrMessages(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnNoLinksOrMessages value = DeleteOnNoLinksOrMessages.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoLinksOrMessages);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksOrMessagesWithList8() throws IOException {
+        doTestDecodeDeleteOnNoLinksOrMessagesWithList8(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksOrMessagesWithList8FromStream() throws IOException {
+        doTestDecodeDeleteOnNoLinksOrMessagesWithList8(true);
+    }
+
+    private void doTestDecodeDeleteOnNoLinksOrMessagesWithList8(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoLinksOrMessages);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksOrMessagesWithList32() throws IOException {
+        doTestDecodeDeleteOnNoLinksOrMessagesWithList32(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksOrMessagesWithList32FromStream() throws IOException {
+        doTestDecodeDeleteOnNoLinksOrMessagesWithList32(true);
+    }
+
+    private void doTestDecodeDeleteOnNoLinksOrMessagesWithList32(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoLinksOrMessages);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, DeleteOnNoLinksOrMessages.getInstance());
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(DeleteOnNoLinksOrMessages.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(DeleteOnNoLinksOrMessages.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinksOrMessages.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(DeleteOnNoLinksOrMessages.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(DeleteOnNoLinksOrMessages.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnNoLinksOrMessages[] array = new DeleteOnNoLinksOrMessages[3];
+
+        array[0] = DeleteOnNoLinksOrMessages.getInstance();
+        array[1] = DeleteOnNoLinksOrMessages.getInstance();
+        array[2] = DeleteOnNoLinksOrMessages.getInstance();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(DeleteOnNoLinksOrMessages.class, result.getClass().getComponentType());
+
+        DeleteOnNoLinksOrMessages[] resultArray = (DeleteOnNoLinksOrMessages[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof DeleteOnNoLinksOrMessages);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoLinksTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoLinksTypeCodecTest.java
new file mode 100644
index 0000000..39b5380
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoLinksTypeCodecTest.java
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoLinksTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnNoLinksTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoLinks;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of DeleteOnNoLinks types.
+ */
+public class DeleteOnNoLinksTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(DeleteOnNoLinks.class, new DeleteOnNoLinksTypeDecoder().getTypeClass());
+        assertEquals(DeleteOnNoLinks.class, new DeleteOnNoLinksTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(DeleteOnNoLinks.DESCRIPTOR_CODE, new DeleteOnNoLinksTypeDecoder().getDescriptorCode());
+        assertEquals(DeleteOnNoLinks.DESCRIPTOR_CODE, new DeleteOnNoLinksTypeEncoder().getDescriptorCode());
+        assertEquals(DeleteOnNoLinks.DESCRIPTOR_SYMBOL, new DeleteOnNoLinksTypeDecoder().getDescriptorSymbol());
+        assertEquals(DeleteOnNoLinks.DESCRIPTOR_SYMBOL, new DeleteOnNoLinksTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinks() throws IOException {
+        doTestDecodeDeleteOnNoLinks(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksFromStream() throws IOException {
+        doTestDecodeDeleteOnNoLinks(true);
+    }
+
+    private void doTestDecodeDeleteOnNoLinks(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnNoLinks value = DeleteOnNoLinks.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoLinks);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksWithList8() throws IOException {
+        doTestDecodeDeleteOnNoLinksWithList8(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksWithList8FromStream() throws IOException {
+        doTestDecodeDeleteOnNoLinksWithList8(true);
+    }
+
+    private void doTestDecodeDeleteOnNoLinksWithList8(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinks.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoLinks);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksWithList32() throws IOException {
+        doTestDecodeDeleteOnNoLinksWithList32(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoLinksWithList32FromStream() throws IOException {
+        doTestDecodeDeleteOnNoLinksWithList32(true);
+    }
+
+    private void doTestDecodeDeleteOnNoLinksWithList32(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinks.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoLinks);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, DeleteOnNoLinks.getInstance());
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(DeleteOnNoLinks.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(DeleteOnNoLinks.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinks.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoLinks.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(DeleteOnNoLinks.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(DeleteOnNoLinks.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnNoLinks[] array = new DeleteOnNoLinks[3];
+
+        array[0] = DeleteOnNoLinks.getInstance();
+        array[1] = DeleteOnNoLinks.getInstance();
+        array[2] = DeleteOnNoLinks.getInstance();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(DeleteOnNoLinks.class, result.getClass().getComponentType());
+
+        DeleteOnNoLinks[] resultArray = (DeleteOnNoLinks[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof DeleteOnNoLinks);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoMessagesTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoMessagesTypeCodecTest.java
new file mode 100644
index 0000000..47b1522
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeleteOnNoMessagesTypeCodecTest.java
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeleteOnNoMessagesTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeleteOnNoMessagesTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.DeleteOnNoMessages;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of DeleteOnNoMessages types.
+ */
+public class DeleteOnNoMessagesTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(DeleteOnNoMessages.class, new DeleteOnNoMessagesTypeDecoder().getTypeClass());
+        assertEquals(DeleteOnNoMessages.class, new DeleteOnNoMessagesTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(DeleteOnNoMessages.DESCRIPTOR_CODE, new DeleteOnNoMessagesTypeDecoder().getDescriptorCode());
+        assertEquals(DeleteOnNoMessages.DESCRIPTOR_CODE, new DeleteOnNoMessagesTypeEncoder().getDescriptorCode());
+        assertEquals(DeleteOnNoMessages.DESCRIPTOR_SYMBOL, new DeleteOnNoMessagesTypeDecoder().getDescriptorSymbol());
+        assertEquals(DeleteOnNoMessages.DESCRIPTOR_SYMBOL, new DeleteOnNoMessagesTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoMessages() throws IOException {
+        doTestDecodeDeleteOnNoMessages(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoMessagesFromStream() throws IOException {
+        doTestDecodeDeleteOnNoMessages(true);
+    }
+
+    private void doTestDecodeDeleteOnNoMessages(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnNoMessages value = DeleteOnNoMessages.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoMessages);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoMessagesWithList8() throws IOException {
+        doTestDecodeDeleteOnNoMessagesWithList8(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoMessagesWithList8FromStream() throws IOException {
+        doTestDecodeDeleteOnNoMessagesWithList8(true);
+    }
+
+    private void doTestDecodeDeleteOnNoMessagesWithList8(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoMessages.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoMessages);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoMessagesWithList32() throws IOException {
+        doTestDecodeDeleteOnNoMessagesWithList32(false);
+    }
+
+    @Test
+    public void testDecodeDeleteOnNoMessagesWithList32FromStream() throws IOException {
+        doTestDecodeDeleteOnNoMessagesWithList32(true);
+    }
+
+    private void doTestDecodeDeleteOnNoMessagesWithList32(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoMessages.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeleteOnNoMessages);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, DeleteOnNoMessages.getInstance());
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(DeleteOnNoMessages.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(DeleteOnNoMessages.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoMessages.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeleteOnNoMessages.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(DeleteOnNoMessages.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(DeleteOnNoMessages.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeleteOnNoMessages[] array = new DeleteOnNoMessages[3];
+
+        array[0] = DeleteOnNoMessages.getInstance();
+        array[1] = DeleteOnNoMessages.getInstance();
+        array[2] = DeleteOnNoMessages.getInstance();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(DeleteOnNoMessages.class, result.getClass().getComponentType());
+
+        DeleteOnNoMessages[] resultArray = (DeleteOnNoMessages[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof DeleteOnNoMessages);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeliveryAnnotationsTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeliveryAnnotationsTypeCodecTest.java
new file mode 100644
index 0000000..b0e96d2
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/DeliveryAnnotationsTypeCodecTest.java
@@ -0,0 +1,533 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.DeliveryAnnotationsTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.DeliveryAnnotationsTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.DeliveryAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+public class DeliveryAnnotationsTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(DeliveryAnnotations.class, new DeliveryAnnotationsTypeDecoder().getTypeClass());
+        assertEquals(DeliveryAnnotations.class, new DeliveryAnnotationsTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(DeliveryAnnotations.DESCRIPTOR_CODE, new DeliveryAnnotationsTypeDecoder().getDescriptorCode());
+        assertEquals(DeliveryAnnotations.DESCRIPTOR_CODE, new DeliveryAnnotationsTypeEncoder().getDescriptorCode());
+        assertEquals(DeliveryAnnotations.DESCRIPTOR_SYMBOL, new DeliveryAnnotationsTypeDecoder().getDescriptorSymbol());
+        assertEquals(DeliveryAnnotations.DESCRIPTOR_SYMBOL, new DeliveryAnnotationsTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfDeliveryAnnotations() throws IOException {
+        doTestDecodeDeliveryAnnotationsSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfDeliveryAnnotations() throws IOException {
+        doTestDecodeDeliveryAnnotationsSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeDeliveryAnnotations() throws IOException {
+        doTestDecodeDeliveryAnnotationsSeries(1, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfDeliveryAnnotationsFromStream() throws IOException {
+        doTestDecodeDeliveryAnnotationsSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfDeliveryAnnotationsFromStream() throws IOException {
+        doTestDecodeDeliveryAnnotationsSeries(LARGE_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeDeliveryAnnotationsFromStream() throws IOException {
+        doTestDecodeDeliveryAnnotationsSeries(1, true);
+    }
+
+    private void doTestDecodeDeliveryAnnotationsSeries(int size, boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("test2");
+        final Symbol SYMBOL_3 = Symbol.valueOf("test3");
+        final Symbol SYMBOL_4 = Symbol.valueOf("test4");
+
+        DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, UnsignedByte.valueOf((byte) 128));
+        annotations.getValue().put(SYMBOL_2, UnsignedShort.valueOf((short) 128));
+        annotations.getValue().put(SYMBOL_3, UnsignedInteger.valueOf(128));
+        annotations.getValue().put(SYMBOL_4, UnsignedLong.valueOf(128));
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, annotations);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof DeliveryAnnotations);
+
+            DeliveryAnnotations readAnnotations = (DeliveryAnnotations) result;
+
+            Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+            assertEquals(annotations.getValue().size(), resultMap.size());
+            assertEquals(resultMap.get(SYMBOL_1), UnsignedByte.valueOf((byte) 128));
+            assertEquals(resultMap.get(SYMBOL_2), UnsignedShort.valueOf((short) 128));
+            assertEquals(resultMap.get(SYMBOL_3), UnsignedInteger.valueOf(128));
+            assertEquals(resultMap.get(SYMBOL_4), UnsignedLong.valueOf(128));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeDeliveryAnnotationsArray() throws IOException {
+        doTestEncodeDecodeDeliveryAnnotationsArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeDeliveryAnnotationsArrayFromStream() throws IOException {
+        doTestEncodeDecodeDeliveryAnnotationsArray(true);
+    }
+
+    private void doTestEncodeDecodeDeliveryAnnotationsArray(boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("test2");
+        final Symbol SYMBOL_3 = Symbol.valueOf("test3");
+
+        DeliveryAnnotations[] array = new DeliveryAnnotations[3];
+
+        DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, UnsignedByte.valueOf((byte) 128));
+        annotations.getValue().put(SYMBOL_2, UnsignedShort.valueOf((short) 128));
+        annotations.getValue().put(SYMBOL_3, UnsignedInteger.valueOf(128));
+
+        array[0] = annotations;
+        array[1] = annotations;
+        array[2] = annotations;
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(DeliveryAnnotations.class, result.getClass().getComponentType());
+
+        DeliveryAnnotations[] resultArray = (DeliveryAnnotations[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            DeliveryAnnotations readAnnotations = resultArray[i];
+
+            Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+            assertEquals(annotations.getValue().size(), resultMap.size());
+            assertEquals(resultMap.get(SYMBOL_1), UnsignedByte.valueOf((byte) 128));
+            assertEquals(resultMap.get(SYMBOL_2), UnsignedShort.valueOf((short) 128));
+            assertEquals(resultMap.get(SYMBOL_3), UnsignedInteger.valueOf(128));
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> map = new HashMap<>();
+        map.put(Symbol.valueOf("one"), 1);
+        map.put(Symbol.valueOf("two"), Boolean.TRUE);
+        map.put(Symbol.valueOf("three"), "test");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new DeliveryAnnotations(map));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(DeliveryAnnotations.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(DeliveryAnnotations.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValue() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(false);
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValueFromStream() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(true);
+    }
+
+    private void doTestEncodeDecodeMessageAnnotationsWithEmptyValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, new DeliveryAnnotations(null));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeliveryAnnotations);
+
+        DeliveryAnnotations readAnnotations = (DeliveryAnnotations) result;
+        assertNull(readAnnotations.getValue());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, true);
+    }
+
+    private void doTestSkipValueWithInvalidListType(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeliveryAnnotations.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8){
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(DeliveryAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(DeliveryAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncoding() throws IOException {
+        doTestSkipValueWithNullMapEncoding(false);
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncodingFromStream() throws IOException {
+        doTestSkipValueWithNullMapEncoding(true);
+    }
+
+    private void doTestSkipValueWithNullMapEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeliveryAnnotations.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(DeliveryAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(DeliveryAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        DeliveryAnnotations[] array = new DeliveryAnnotations[3];
+
+        Map<Symbol, Object> map = new HashMap<>();
+        map.put(Symbol.valueOf("1"), Boolean.TRUE);
+        map.put(Symbol.valueOf("2"), Boolean.FALSE);
+
+        array[0] = new DeliveryAnnotations(new HashMap<>());
+        array[1] = new DeliveryAnnotations(map);
+        array[2] = new DeliveryAnnotations(map);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(DeliveryAnnotations.class, result.getClass().getComponentType());
+
+        DeliveryAnnotations[] resultArray = (DeliveryAnnotations[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof DeliveryAnnotations);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeAnnoationsWithEmbeddedMaps() throws IOException {
+        doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeAnnoationsWithEmbeddedMapsFromStream() throws IOException {
+        doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(true);
+    }
+
+    public void doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("x-opt-test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("x-opt-test2");
+
+        final String VALUE_1 = "string";
+        final UnsignedInteger VALUE_2 = UnsignedInteger.valueOf(42);
+        final UUID VALUE_3 = UUID.randomUUID();
+
+        Map<String, Object> stringKeyedMap = new HashMap<>();
+        stringKeyedMap.put("key1", VALUE_1);
+        stringKeyedMap.put("key2", VALUE_2);
+        stringKeyedMap.put("key3", VALUE_3);
+
+        Map<Symbol, Object> symbolKeyedMap = new HashMap<>();
+        symbolKeyedMap.put(Symbol.valueOf("key1"), VALUE_1);
+        symbolKeyedMap.put(Symbol.valueOf("key2"), VALUE_2);
+        symbolKeyedMap.put(Symbol.valueOf("key3"), VALUE_3);
+
+        DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, stringKeyedMap);
+        annotations.getValue().put(SYMBOL_2, symbolKeyedMap);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, annotations);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeliveryAnnotations);
+
+        DeliveryAnnotations readAnnotations = (DeliveryAnnotations) result;
+
+        Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+        assertEquals(annotations.getValue().size(), resultMap.size());
+        assertEquals(resultMap.get(SYMBOL_1), stringKeyedMap);
+        assertEquals(resultMap.get(SYMBOL_2), symbolKeyedMap);
+    }
+
+    @Test
+    public void testReadTypeWithNullEncoding() throws IOException {
+        testReadTypeWithNullEncoding(false);
+    }
+
+    @Test
+    public void testReadTypeWithNullEncodingFromStream() throws IOException {
+        testReadTypeWithNullEncoding(true);
+    }
+
+    private void testReadTypeWithNullEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeliveryAnnotations.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof DeliveryAnnotations);
+
+        DeliveryAnnotations decoded = (DeliveryAnnotations) result;
+        assertNull(decoded.getValue());
+    }
+
+    @Test
+    public void testReadTypeWithOverLargeEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(DeliveryAnnotations.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.MAP32);
+        buffer.writeInt(Integer.MAX_VALUE);  // Size
+        buffer.writeInt(4);  // Count
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not decode type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/FlowTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/FlowTypeCodecTest.java
new file mode 100644
index 0000000..9b44647
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/FlowTypeCodecTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.FlowTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.FlowTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.junit.jupiter.api.Test;
+
+public class FlowTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Flow.class, new FlowTypeDecoder().getTypeClass());
+        assertEquals(Flow.class, new FlowTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Flow.DESCRIPTOR_CODE, new FlowTypeDecoder().getDescriptorCode());
+        assertEquals(Flow.DESCRIPTOR_CODE, new FlowTypeEncoder().getDescriptorCode());
+        assertEquals(Flow.DESCRIPTOR_SYMBOL, new FlowTypeDecoder().getDescriptorSymbol());
+        assertEquals(Flow.DESCRIPTOR_SYMBOL, new FlowTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeFlow() throws IOException {
+        doTestDecodeFlowSeries(1);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfFlows() throws IOException {
+        doTestDecodeFlowSeries(SMALL_SIZE);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfFlows() throws IOException {
+        doTestDecodeFlowSeries(LARGE_SIZE);
+    }
+
+    private void doTestDecodeFlowSeries(int size) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Flow flow = new Flow();
+        flow.setNextIncomingId(1);
+        flow.setIncomingWindow(2047);
+        flow.setNextOutgoingId(1);
+        flow.setOutgoingWindow(UnsignedInteger.MAX_VALUE.longValue());
+        flow.setHandle(0);
+        flow.setDeliveryCount(10);
+        flow.setLinkCredit(1000);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, flow);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result = decoder.readObject(buffer, decoderState);
+
+            assertNotNull(result);
+            assertTrue(result instanceof Flow);
+
+            Flow decoded = (Flow) result;
+
+            assertEquals(flow.getNextIncomingId(), decoded.getNextIncomingId());
+            assertEquals(flow.getIncomingWindow(), decoded.getIncomingWindow());
+            assertEquals(flow.getNextOutgoingId(), decoded.getNextOutgoingId());
+            assertEquals(flow.getOutgoingWindow(), decoded.getOutgoingWindow());
+            assertEquals(flow.getHandle(), decoded.getHandle());
+            assertEquals(flow.getDeliveryCount(), decoded.getDeliveryCount());
+            assertEquals(flow.getLinkCredit(), decoded.getLinkCredit());
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfDataSections() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Flow flow = new Flow();
+        flow.setNextIncomingId(1);
+        flow.setIncomingWindow(2047);
+        flow.setNextOutgoingId(1);
+        flow.setOutgoingWindow(UnsignedInteger.MAX_VALUE.longValue());
+        flow.setHandle(0);
+        flow.setDeliveryCount(10);
+        flow.setLinkCredit(1000);
+
+        Flow[] flowArray = new Flow[3];
+
+        flowArray[0] = flow;
+        flowArray[1] = flow;
+        flowArray[2] = flow;
+
+        encoder.writeObject(buffer, encoderState, flowArray);
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Flow.class, result.getClass().getComponentType());
+
+        Flow[] resultArray = (Flow[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Flow);
+            assertEquals(flowArray[i].getNextIncomingId(), resultArray[i].getNextIncomingId());
+            assertEquals(flowArray[i].getIncomingWindow(), resultArray[i].getIncomingWindow());
+            assertEquals(flowArray[i].getNextOutgoingId(), resultArray[i].getNextOutgoingId());
+            assertEquals(flowArray[i].getOutgoingWindow(), resultArray[i].getOutgoingWindow());
+            assertEquals(flowArray[i].getHandle(), resultArray[i].getHandle());
+            assertEquals(flowArray[i].getDeliveryCount(), resultArray[i].getDeliveryCount());
+            assertEquals(flowArray[i].getLinkCredit(), resultArray[i].getLinkCredit());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new Flow().setAvailable(100));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Flow.class, typeDecoder.getTypeClass());
+            typeDecoder.skipValue(buffer, decoderState);
+        }
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Flow.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not decode type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Flow.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertEquals(Flow.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.skipValue(buffer, decoderState);
+            fail("Should not be able to skip type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Flow[] array = new Flow[3];
+
+        array[0] = new Flow();
+        array[1] = new Flow();
+        array[2] = new Flow();
+
+        array[0].setHandle(0).setLinkCredit(0).setDeliveryCount(1).setIncomingWindow(1024).setNextOutgoingId(1).setOutgoingWindow(128);
+        array[1].setHandle(1).setLinkCredit(1).setDeliveryCount(1).setIncomingWindow(2048).setNextOutgoingId(2).setOutgoingWindow(256);
+        array[2].setHandle(2).setLinkCredit(2).setDeliveryCount(1).setIncomingWindow(4096).setNextOutgoingId(3).setOutgoingWindow(512);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Flow.class, result.getClass().getComponentType());
+
+        Flow[] resultArray = (Flow[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Flow);
+            assertEquals(array[i].getHandle(), resultArray[i].getHandle());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/FooterTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/FooterTypeCodecTest.java
new file mode 100644
index 0000000..76edc05
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/FooterTypeCodecTest.java
@@ -0,0 +1,505 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.FooterTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.FooterTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Footer;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+public class FooterTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Footer.class, new FooterTypeEncoder().getTypeClass());
+        assertEquals(Footer.class, new FooterTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Footer.DESCRIPTOR_CODE, new FooterTypeEncoder().getDescriptorCode());
+        assertEquals(Footer.DESCRIPTOR_CODE, new FooterTypeDecoder().getDescriptorCode());
+        assertEquals(Footer.DESCRIPTOR_SYMBOL, new FooterTypeEncoder().getDescriptorSymbol());
+        assertEquals(Footer.DESCRIPTOR_SYMBOL, new FooterTypeDecoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfFooter() throws IOException {
+        doTestDecodeHeaderSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfFooter() throws IOException {
+        doTestDecodeHeaderSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfFooterFromStream() throws IOException {
+        doTestDecodeHeaderSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfFooterFromStream() throws IOException {
+        doTestDecodeHeaderSeries(LARGE_SIZE, false);
+    }
+
+    private void doTestDecodeHeaderSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> propertiesMap = new LinkedHashMap<>();
+        Footer properties = new Footer(propertiesMap);
+
+        propertiesMap.put(Symbol.valueOf("key-1"), "1");
+        propertiesMap.put(Symbol.valueOf("key-2"), "2");
+        propertiesMap.put(Symbol.valueOf("key-3"), "3");
+        propertiesMap.put(Symbol.valueOf("key-4"), "4");
+        propertiesMap.put(Symbol.valueOf("key-5"), "5");
+        propertiesMap.put(Symbol.valueOf("key-6"), "6");
+        propertiesMap.put(Symbol.valueOf("key-7"), "7");
+        propertiesMap.put(Symbol.valueOf("key-8"), "8");
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Footer);
+
+            Footer decoded = (Footer) result;
+
+            assertEquals(8, decoded.getValue().size());
+            assertTrue(decoded.getValue().equals(propertiesMap));
+        }
+    }
+
+    @Test
+    public void testDecodeFailsWhenDescriedValueIsNotMapType() throws IOException {
+        doTestDecodeFailsWhenDescriedValueIsNotMapType(false);
+    }
+
+    @Test
+    public void testDecodeFailsWhenDescriedValueIsNotMapTypeFromStream() throws IOException {
+        doTestDecodeFailsWhenDescriedValueIsNotMapType(true);
+    }
+
+    private void doTestDecodeFailsWhenDescriedValueIsNotMapType(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Footer.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt(0);
+        buffer.writeInt(0);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithNullBodyUsingDescriptorCode() throws IOException {
+        doTestDecodeWithNullBodyUsingDescriptorCode(false);
+    }
+
+    @Test
+    public void testDecodeWithNullBodyUsingDescriptorCodeFromStream() throws IOException {
+        doTestDecodeWithNullBodyUsingDescriptorCode(true);
+    }
+
+    private void doTestDecodeWithNullBodyUsingDescriptorCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Footer.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Footer result;
+        if (fromStream) {
+            result = (Footer) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Footer) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNull(result.getValue());
+    }
+
+    @Test
+    public void testDecodeWithNullBodyUsingDescriptorSymbol() throws IOException {
+        testDecodeWithNullBodyUsingDescriptorSymbol(false);
+    }
+
+    @Test
+    public void testDecodeWithNullBodyUsingDescriptorSymbolFromStream() throws IOException {
+        testDecodeWithNullBodyUsingDescriptorSymbol(true);
+    }
+
+    private void testDecodeWithNullBodyUsingDescriptorSymbol(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SYM8);
+        buffer.writeByte(Footer.DESCRIPTOR_SYMBOL.getLength());
+        Footer.DESCRIPTOR_SYMBOL.writeTo(buffer);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Footer result;
+        if (fromStream) {
+            result = (Footer) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Footer) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNull(result.getValue());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> map = new HashMap<>();
+        map.put(Symbol.valueOf("one"), 1);
+        map.put(Symbol.valueOf("two"), Boolean.TRUE);
+        map.put(Symbol.valueOf("three"), "test");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new Footer(map));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Footer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Footer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValue() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(false);
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValueFromStream() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(true);
+    }
+
+    private void doTestEncodeDecodeMessageAnnotationsWithEmptyValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, new Footer(null));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Footer);
+
+        Footer readAnnotations = (Footer) result;
+        assertNull(readAnnotations.getValue());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, true);
+    }
+
+    private void doTestSkipValueWithInvalidListType(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Footer.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8){
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Footer.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Footer.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncoding() throws IOException {
+        doTestSkipValueWithNullMapEncoding(false);
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncodingFromStream() throws IOException {
+        doTestSkipValueWithNullMapEncoding(true);
+    }
+
+    private void doTestSkipValueWithNullMapEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Footer.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Footer.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Footer.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Footer[] array = new Footer[3];
+
+        Map<Symbol, Object> map = new HashMap<>();
+        map.put(Symbol.valueOf("1"), Boolean.TRUE);
+        map.put(Symbol.valueOf("2"), Boolean.FALSE);
+
+        array[0] = new Footer(new HashMap<>());
+        array[1] = new Footer(map);
+        array[2] = new Footer(map);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Footer.class, result.getClass().getComponentType());
+
+        Footer[] resultArray = (Footer[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Footer);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeAnnoationsWithEmbeddedMaps() throws IOException {
+        doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeAnnoationsWithEmbeddedMapsFromStream() throws IOException {
+        doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(true);
+    }
+
+    private void doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("x-opt-test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("x-opt-test2");
+
+        final String VALUE_1 = "string";
+        final UnsignedInteger VALUE_2 = UnsignedInteger.valueOf(42);
+        final UUID VALUE_3 = UUID.randomUUID();
+
+        Map<String, Object> stringKeyedMap = new HashMap<>();
+        stringKeyedMap.put("key1", VALUE_1);
+        stringKeyedMap.put("key2", VALUE_2);
+        stringKeyedMap.put("key3", VALUE_3);
+
+        Map<Symbol, Object> symbolKeyedMap = new HashMap<>();
+        symbolKeyedMap.put(Symbol.valueOf("key1"), VALUE_1);
+        symbolKeyedMap.put(Symbol.valueOf("key2"), VALUE_2);
+        symbolKeyedMap.put(Symbol.valueOf("key3"), VALUE_3);
+
+        Footer annotations = new Footer(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, stringKeyedMap);
+        annotations.getValue().put(SYMBOL_2, symbolKeyedMap);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, annotations);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Footer);
+
+        Footer readAnnotations = (Footer) result;
+
+        Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+        assertEquals(annotations.getValue().size(), resultMap.size());
+        assertEquals(resultMap.get(SYMBOL_1), stringKeyedMap);
+        assertEquals(resultMap.get(SYMBOL_2), symbolKeyedMap);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/HeaderTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/HeaderTypeCodecTest.java
new file mode 100644
index 0000000..81787b7
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/HeaderTypeCodecTest.java
@@ -0,0 +1,554 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.HeaderTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.HeaderTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Header;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for decoder of AMQP Header type.
+ */
+public class HeaderTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Header.class, new HeaderTypeDecoder().getTypeClass());
+        assertEquals(Header.class, new HeaderTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Header.DESCRIPTOR_CODE, new HeaderTypeDecoder().getDescriptorCode());
+        assertEquals(Header.DESCRIPTOR_CODE, new HeaderTypeEncoder().getDescriptorCode());
+        assertEquals(Header.DESCRIPTOR_SYMBOL, new HeaderTypeDecoder().getDescriptorSymbol());
+        assertEquals(Header.DESCRIPTOR_SYMBOL, new HeaderTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeHeader() throws IOException {
+        doTestDecodeHeaderSeries(1, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfHeaders() throws IOException {
+        doTestDecodeHeaderSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfHeaders() throws IOException {
+        doTestDecodeHeaderSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeHeaderFromStream() throws IOException {
+        doTestDecodeHeaderSeries(1, true);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfHeadersFromStream() throws IOException {
+        doTestDecodeHeaderSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfHeadersFromStream() throws IOException {
+        doTestDecodeHeaderSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeHeaderSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Header header = new Header();
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomDeliveryCount = random.nextInt();
+        final int randomTimeToLive = random.nextInt();
+
+        header.setDurable(Boolean.TRUE);
+        header.setPriority((byte) 3);
+        header.setDeliveryCount(randomDeliveryCount);
+        header.setFirstAcquirer(Boolean.TRUE);
+        header.setTimeToLive(randomTimeToLive);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, header);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Header);
+
+            Header decoded = (Header) result;
+
+            assertEquals(3, decoded.getPriority());
+            assertEquals(Integer.toUnsignedLong(randomTimeToLive), decoded.getTimeToLive());
+            assertEquals(Integer.toUnsignedLong(randomDeliveryCount), decoded.getDeliveryCount());
+            assertTrue(decoded.isDurable());
+            assertTrue(decoded.isFirstAcquirer());
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeWithMaxUnsignedValuesFromLongs() throws IOException {
+        doTestEncodeAndDecodeWithMaxUnsignedValuesFromLongs(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeWithMaxUnsignedValuesFromLongsFromStream() throws IOException {
+        doTestEncodeAndDecodeWithMaxUnsignedValuesFromLongs(true);
+    }
+
+    private void doTestEncodeAndDecodeWithMaxUnsignedValuesFromLongs(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+        final Header header = new Header();
+
+        header.setDeliveryCount(UnsignedInteger.MAX_VALUE.longValue());
+        header.setTimeToLive(UnsignedInteger.MAX_VALUE.longValue());
+
+        encoder.writeObject(buffer, encoderState, header);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Header);
+
+        Header decoded = (Header) result;
+
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), decoded.getDeliveryCount());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), decoded.getTimeToLive());
+    }
+
+    @Test
+    public void testEncodeDecodeZeroSizedArrayOfHeaders() throws IOException {
+        dotestEncodeDecodeZeroSizedArrayOfHeaders(false);
+    }
+
+    @Test
+    public void testEncodeDecodeZeroSizedArrayOfHeadersFromStream() throws IOException {
+        dotestEncodeDecodeZeroSizedArrayOfHeaders(true);
+    }
+
+    private void dotestEncodeDecodeZeroSizedArrayOfHeaders(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Header[] headerArray = new Header[0];
+
+        encoder.writeObject(buffer, encoderState, headerArray);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Header.class, result.getClass().getComponentType());
+
+        Header[] resultArray = (Header[]) result;
+        assertEquals(0, resultArray.length);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfHeaders() throws IOException {
+        doTestEncodeDecodeArrayOfHeaders(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayOfHeadersFromStream() throws IOException {
+        doTestEncodeDecodeArrayOfHeaders(true);
+    }
+
+    private void doTestEncodeDecodeArrayOfHeaders(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Header[] headerArray = new Header[3];
+
+        headerArray[0] = new Header();
+        headerArray[1] = new Header();
+        headerArray[2] = new Header();
+
+        headerArray[0].setDurable(true);
+        headerArray[1].setDurable(true);
+        headerArray[2].setDurable(true);
+
+        encoder.writeObject(buffer, encoderState, headerArray);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Header.class, result.getClass().getComponentType());
+
+        Header[] resultArray = (Header[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Header);
+            assertEquals(headerArray[i].isDurable(), resultArray[i].isDurable());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Header header = new Header();
+
+        header.setDurable(Boolean.TRUE);
+        header.setPriority((byte) 3);
+        header.setDeliveryCount(10);
+        header.setFirstAcquirer(Boolean.FALSE);
+        header.setTimeToLive(500);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new Header());
+        }
+
+        encoder.writeObject(buffer, encoderState, header);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Header.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Header.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Header);
+
+        Header value = (Header) result;
+        assertEquals(3, value.getPriority());
+        assertTrue(value.isDurable());
+        assertFalse(value.isFirstAcquirer());
+        assertEquals(500, header.getTimeToLive());
+        assertEquals(10, header.getDeliveryCount());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Header.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Header.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Header.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Header.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeFailsWhenList8IndicateToManyEntries() throws IOException {
+        doTestDecodeFailsWhenListIndicateToManyEntries(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeFailsWhenList32IndicateToManyEntries() throws IOException {
+        doTestDecodeFailsWhenListIndicateToManyEntries(EncodingCodes.LIST32, true);
+    }
+
+    @Test
+    public void testDecodeFailsWhenList8IndicateToManyEntriesFromStream() throws IOException {
+        doTestDecodeFailsWhenListIndicateToManyEntries(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeFailsWhenList32IndicateToManyEntriesFromStream() throws IOException {
+        doTestDecodeFailsWhenListIndicateToManyEntries(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeFailsWhenListIndicateToManyEntries(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Header.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 20);  // Size
+            buffer.writeInt((byte) 10);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 20);  // Size
+            buffer.writeByte((byte) 10);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Header.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.readValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Header.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.readValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeFailsWhenList8IndicateOverflowedEntries() throws IOException {
+        doTestDecodeFailsWhenListIndicateOverflowedEntries(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeFailsWhenList32IndicateOverflowedEntries() throws IOException {
+        doTestDecodeFailsWhenListIndicateOverflowedEntries(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeFailsWhenList8IndicateOverflowedEntriesFromStream() throws IOException {
+        doTestDecodeFailsWhenListIndicateOverflowedEntries(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeFailsWhenList32IndicateOverflowedEntriesFromStream() throws IOException {
+        doTestDecodeFailsWhenListIndicateOverflowedEntries(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeFailsWhenListIndicateOverflowedEntries(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Header.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(20);  // Size
+            buffer.writeInt(-1);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 20);  // Size
+            buffer.writeByte((byte) 255);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Header.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.readValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Header.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.readValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Header[] array = new Header[3];
+
+        array[0] = new Header();
+        array[1] = new Header();
+        array[2] = new Header();
+
+        array[0].setPriority((byte) 1).setDeliveryCount(1);
+        array[1].setPriority((byte) 2).setDeliveryCount(2);
+        array[2].setPriority((byte) 3).setDeliveryCount(3);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Header.class, result.getClass().getComponentType());
+
+        Header[] resultArray = (Header[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Header);
+            assertEquals(array[i].getPriority(), resultArray[i].getPriority());
+            assertEquals(array[i].getDeliveryCount(), resultArray[i].getDeliveryCount());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/MessageAnnotationsTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/MessageAnnotationsTypeCodecTest.java
new file mode 100644
index 0000000..422d46d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/MessageAnnotationsTypeCodecTest.java
@@ -0,0 +1,528 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.MessageAnnotationsTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.MessageAnnotationsTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+public class MessageAnnotationsTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(MessageAnnotations.class, new MessageAnnotationsTypeDecoder().getTypeClass());
+        assertEquals(MessageAnnotations.class, new MessageAnnotationsTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(MessageAnnotations.DESCRIPTOR_CODE, new MessageAnnotationsTypeDecoder().getDescriptorCode());
+        assertEquals(MessageAnnotations.DESCRIPTOR_CODE, new MessageAnnotationsTypeEncoder().getDescriptorCode());
+        assertEquals(MessageAnnotations.DESCRIPTOR_SYMBOL, new MessageAnnotationsTypeDecoder().getDescriptorSymbol());
+        assertEquals(MessageAnnotations.DESCRIPTOR_SYMBOL, new MessageAnnotationsTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfMessageAnnotations() throws IOException {
+        doTestDecodeMessageAnnotationsSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfMessageAnnotations() throws IOException {
+        doTestDecodeMessageAnnotationsSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeMessageAnnotations() throws IOException {
+        doTestDecodeMessageAnnotationsSeries(1, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfMessageAnnotationsFromStream() throws IOException {
+        doTestDecodeMessageAnnotationsSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfMessageAnnotationsFromStream() throws IOException {
+        doTestDecodeMessageAnnotationsSeries(LARGE_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeMessageAnnotationsFromStream() throws IOException {
+        doTestDecodeMessageAnnotationsSeries(1, true);
+    }
+
+    private void doTestDecodeMessageAnnotationsSeries(int size, boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("test2");
+        final Symbol SYMBOL_3 = Symbol.valueOf("test3");
+
+        MessageAnnotations annotations = new MessageAnnotations(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, UnsignedByte.valueOf((byte) 128));
+        annotations.getValue().put(SYMBOL_2, UnsignedShort.valueOf((short) 128));
+        annotations.getValue().put(SYMBOL_3, UnsignedInteger.valueOf(128));
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, annotations);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof MessageAnnotations);
+
+            MessageAnnotations readAnnotations = (MessageAnnotations) result;
+
+            Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+            assertEquals(annotations.getValue().size(), resultMap.size());
+            assertEquals(resultMap.get(SYMBOL_1), UnsignedByte.valueOf((byte) 128));
+            assertEquals(resultMap.get(SYMBOL_2), UnsignedShort.valueOf((short) 128));
+            assertEquals(resultMap.get(SYMBOL_3), UnsignedInteger.valueOf(128));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsArray() throws IOException {
+        doTstEncodeDecodeMessageAnnotationsArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsArrayFromStream() throws IOException {
+        doTstEncodeDecodeMessageAnnotationsArray(true);
+    }
+
+    private void doTstEncodeDecodeMessageAnnotationsArray(boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("test2");
+        final Symbol SYMBOL_3 = Symbol.valueOf("test3");
+
+        MessageAnnotations[] array = new MessageAnnotations[3];
+
+        MessageAnnotations annotations = new MessageAnnotations(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, UnsignedByte.valueOf((byte) 128));
+        annotations.getValue().put(SYMBOL_2, UnsignedShort.valueOf((short) 128));
+        annotations.getValue().put(SYMBOL_3, UnsignedInteger.valueOf(128));
+
+        array[0] = annotations;
+        array[1] = annotations;
+        array[2] = annotations;
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(MessageAnnotations.class, result.getClass().getComponentType());
+
+        MessageAnnotations[] resultArray = (MessageAnnotations[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            MessageAnnotations readAnnotations = resultArray[i];
+
+            Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+            assertEquals(annotations.getValue().size(), resultMap.size());
+            assertEquals(resultMap.get(SYMBOL_1), UnsignedByte.valueOf((byte) 128));
+            assertEquals(resultMap.get(SYMBOL_2), UnsignedShort.valueOf((short) 128));
+            assertEquals(resultMap.get(SYMBOL_3), UnsignedInteger.valueOf(128));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValue() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(false);
+    }
+
+    @Test
+    public void testEncodeDecodeMessageAnnotationsWithEmptyValueFromStream() throws IOException {
+        doTestEncodeDecodeMessageAnnotationsWithEmptyValue(true);
+    }
+
+    private void doTestEncodeDecodeMessageAnnotationsWithEmptyValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, new MessageAnnotations(null));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof MessageAnnotations);
+
+        MessageAnnotations readAnnotations = (MessageAnnotations) result;
+        assertNull(readAnnotations.getValue());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> map = new HashMap<>();
+        map.put(Symbol.valueOf("one"), 1);
+        map.put(Symbol.valueOf("two"), Boolean.TRUE);
+        map.put(Symbol.valueOf("three"), "test");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, new MessageAnnotations(map));
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(MessageAnnotations.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(MessageAnnotations.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0Type() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidList0TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidListType(EncodingCodes.LIST0, true);
+    }
+
+    private void doTestSkipValueWithInvalidListType(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(MessageAnnotations.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8){
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(MessageAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(MessageAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncoding() throws IOException {
+        doTestSkipValueWithNullMapEncoding(false);
+    }
+
+    @Test
+    public void testSkipValueWithNullMapEncodingFromStream() throws IOException {
+        doTestSkipValueWithNullMapEncoding(true);
+    }
+
+    private void doTestSkipValueWithNullMapEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(MessageAnnotations.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(MessageAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(MessageAnnotations.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+            } catch (DecodeException ex) {
+                fail("Should be able to skip type with null inner encoding");
+            }
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        MessageAnnotations[] array = new MessageAnnotations[3];
+
+        Map<Symbol, Object> map = new HashMap<>();
+        map.put(Symbol.valueOf("1"), Boolean.TRUE);
+        map.put(Symbol.valueOf("2"), Boolean.FALSE);
+
+        array[0] = new MessageAnnotations(new HashMap<>());
+        array[1] = new MessageAnnotations(map);
+        array[2] = new MessageAnnotations(map);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(MessageAnnotations.class, result.getClass().getComponentType());
+
+        MessageAnnotations[] resultArray = (MessageAnnotations[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof MessageAnnotations);
+            assertEquals(array[i].getValue(), resultArray[i].getValue());
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeAnnoationsWithEmbeddedMaps() throws IOException {
+        doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeAnnoationsWithEmbeddedMapsFromStream() throws IOException {
+        doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(true);
+    }
+
+    private void doTestEncodeAndDecodeAnnoationsWithEmbeddedMaps(boolean fromStream) throws IOException {
+        final Symbol SYMBOL_1 = Symbol.valueOf("x-opt-test1");
+        final Symbol SYMBOL_2 = Symbol.valueOf("x-opt-test2");
+
+        final String VALUE_1 = "string";
+        final UnsignedInteger VALUE_2 = UnsignedInteger.valueOf(42);
+        final UUID VALUE_3 = UUID.randomUUID();
+
+        Map<String, Object> stringKeyedMap = new HashMap<>();
+        stringKeyedMap.put("key1", VALUE_1);
+        stringKeyedMap.put("key2", VALUE_2);
+        stringKeyedMap.put("key3", VALUE_3);
+
+        Map<Symbol, Object> symbolKeyedMap = new HashMap<>();
+        symbolKeyedMap.put(Symbol.valueOf("key1"), VALUE_1);
+        symbolKeyedMap.put(Symbol.valueOf("key2"), VALUE_2);
+        symbolKeyedMap.put(Symbol.valueOf("key3"), VALUE_3);
+
+        MessageAnnotations annotations = new MessageAnnotations(new HashMap<>());
+        annotations.getValue().put(SYMBOL_1, stringKeyedMap);
+        annotations.getValue().put(SYMBOL_2, symbolKeyedMap);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, annotations);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof MessageAnnotations);
+
+        MessageAnnotations readAnnotations = (MessageAnnotations) result;
+
+        Map<Symbol, Object> resultMap = readAnnotations.getValue();
+
+        assertEquals(annotations.getValue().size(), resultMap.size());
+        assertEquals(resultMap.get(SYMBOL_1), stringKeyedMap);
+        assertEquals(resultMap.get(SYMBOL_2), symbolKeyedMap);
+    }
+
+    @Test
+    public void testReadTypeWithNullEncoding() throws IOException {
+        testReadTypeWithNullEncoding(false);
+    }
+
+    @Test
+    public void testReadTypeWithNullEncodingFromStream() throws IOException {
+        testReadTypeWithNullEncoding(true);
+    }
+
+    private void testReadTypeWithNullEncoding(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(MessageAnnotations.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof MessageAnnotations);
+
+        MessageAnnotations decoded = (MessageAnnotations) result;
+        assertNull(decoded.getValue());
+    }
+
+    @Test
+    public void testReadTypeWithOverLargeEncoding() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(MessageAnnotations.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.MAP32);
+        buffer.writeInt(Integer.MAX_VALUE);  // Size
+        buffer.writeInt(4);  // Count
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not decode type with invalid encoding");
+        } catch (DecodeException ex) {}
+    }}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ModifiedTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ModifiedTypeCodecTest.java
new file mode 100644
index 0000000..2b770b9
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ModifiedTypeCodecTest.java
@@ -0,0 +1,568 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ModifiedTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ModifiedTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of Modified types.
+ */
+public class ModifiedTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Modified.class, new ModifiedTypeDecoder().getTypeClass());
+        assertEquals(Modified.class, new ModifiedTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Modified.DESCRIPTOR_CODE, new ModifiedTypeDecoder().getDescriptorCode());
+        assertEquals(Modified.DESCRIPTOR_CODE, new ModifiedTypeEncoder().getDescriptorCode());
+        assertEquals(Modified.DESCRIPTOR_SYMBOL, new ModifiedTypeDecoder().getDescriptorSymbol());
+        assertEquals(Modified.DESCRIPTOR_SYMBOL, new ModifiedTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeModified() throws IOException {
+        doTestDecodeModified(false);
+    }
+
+    @Test
+    public void testDecodeModifiedFromStream() throws IOException {
+        doTestDecodeModified(true);
+    }
+
+    private void doTestDecodeModified(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Modified value = new Modified();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+
+        value = (Modified) result;
+        assertFalse(value.isDeliveryFailed());
+        assertFalse(value.isUndeliverableHere());
+    }
+
+    @Test
+    public void testDecodeModifiedDeliveryFailed() throws IOException {
+        doTestDecodeModifiedDeliveryFailed(false);
+    }
+
+    @Test
+    public void testDecodeModifiedDeliveryFailedFromStream() throws IOException {
+        doTestDecodeModifiedDeliveryFailed(true);
+    }
+
+    private void doTestDecodeModifiedDeliveryFailed(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Modified value = new Modified();
+        value.setDeliveryFailed(true);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+
+        value = (Modified) result;
+        assertTrue(value.isDeliveryFailed());
+        assertFalse(value.isUndeliverableHere());
+    }
+
+    @Test
+    public void testDecodeModifiedDeliveryFailedUndeliverableHere() throws IOException {
+        doTestDecodeModifiedDeliveryFailedUndeliverableHere(false);
+    }
+
+    @Test
+    public void testDecodeModifiedDeliveryFailedUndeliverableHereFromStream() throws IOException {
+        doTestDecodeModifiedDeliveryFailedUndeliverableHere(true);
+    }
+
+    private void doTestDecodeModifiedDeliveryFailedUndeliverableHere(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Modified value = new Modified();
+        value.setDeliveryFailed(true);
+        value.setUndeliverableHere(true);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+
+        value = (Modified) result;
+        assertTrue(value.isDeliveryFailed());
+        assertTrue(value.isUndeliverableHere());
+    }
+
+    @Test
+    public void testDecodeModifiedWithAnnotations() throws IOException {
+        doTestDecodeModifiedWithAnnotations(false);
+    }
+
+    @Test
+    public void testDecodeModifiedWithAnnotationsFromStream() throws IOException {
+        doTestDecodeModifiedWithAnnotations(true);
+    }
+
+    private void doTestDecodeModifiedWithAnnotations(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> annotations = new LinkedHashMap<>();
+        annotations.put(Symbol.valueOf("test"), "value");
+
+        Modified value = new Modified();
+        value.setMessageAnnotations(annotations);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+
+        value = (Modified) result;
+        assertFalse(value.isDeliveryFailed());
+        assertFalse(value.isUndeliverableHere());
+        assertEquals(annotations, value.getMessageAnnotations());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Modified value = new Modified();
+        value.setDeliveryFailed(true);
+        value.setUndeliverableHere(true);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, value);
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Modified.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Modified.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        value = (Modified) result;
+        assertFalse(value.isUndeliverableHere());
+        assertFalse(value.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeModifiedWithList8() throws IOException {
+        doTestDecodeModifiedWithList8(false);
+    }
+
+    @Test
+    public void testDecodeModifiedWithList8FromStream() throws IOException {
+        doTestDecodeModifiedWithList8(true);
+    }
+
+    private void doTestDecodeModifiedWithList8(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Modified.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+    }
+
+    @Test
+    public void testDecodeModifiedWithList32() throws IOException {
+        doTestDecodeModifiedWithList32(false);
+    }
+
+    @Test
+    public void testDecodeModifiedWithList32FromStream() throws IOException {
+        doTestDecodeModifiedWithList32(true);
+    }
+
+    private void doTestDecodeModifiedWithList32(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Modified.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Modified.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFormStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Modified.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Modified.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Modified.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Modified[] array = new Modified[3];
+
+        array[0] = new Modified();
+        array[1] = new Modified();
+        array[2] = new Modified();
+
+        array[0].setDeliveryFailed(true).setUndeliverableHere(true);
+        array[1].setDeliveryFailed(false).setUndeliverableHere(true);
+        array[2].setDeliveryFailed(false).setUndeliverableHere(false);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Modified.class, result.getClass().getComponentType());
+
+        Modified[] resultArray = (Modified[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Modified);
+            assertEquals(array[i].isDeliveryFailed(), resultArray[i].isDeliveryFailed());
+            assertEquals(array[i].isUndeliverableHere(), resultArray[i].isUndeliverableHere());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Modified.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(-1);  // Count, reads as negative as encoder treats these as signed ints.
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Modified.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/PropertiesTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/PropertiesTypeCodecTest.java
new file mode 100644
index 0000000..34bfabe
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/PropertiesTypeCodecTest.java
@@ -0,0 +1,483 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.PropertiesTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.PropertiesTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for decoder of AMQP Properties type.
+ */
+public class PropertiesTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Properties.class, new PropertiesTypeDecoder().getTypeClass());
+        assertEquals(Properties.class, new PropertiesTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Properties.DESCRIPTOR_CODE, new PropertiesTypeDecoder().getDescriptorCode());
+        assertEquals(Properties.DESCRIPTOR_CODE, new PropertiesTypeEncoder().getDescriptorCode());
+        assertEquals(Properties.DESCRIPTOR_SYMBOL, new PropertiesTypeDecoder().getDescriptorSymbol());
+        assertEquals(Properties.DESCRIPTOR_SYMBOL, new PropertiesTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfPropertiess() throws IOException {
+        doTestDecodePropertiesSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfPropertiess() throws IOException {
+        doTestDecodePropertiesSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfPropertiessFromStream() throws IOException {
+        doTestDecodePropertiesSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfPropertiessStream() throws IOException {
+        doTestDecodePropertiesSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodePropertiesSeries(int size, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomGroupSequence = random.nextInt();
+        final int randomAbsoluteExpiry = random.nextInt();
+        final int randomCreateTime = random.nextInt();
+
+        final Properties properties = new Properties();
+
+        properties.setMessageId("ID:Message-1:1:1:0");
+        properties.setUserId(new Binary(new byte[1]));
+        properties.setTo("queue:work");
+        properties.setSubject("help");
+        properties.setReplyTo("queue:temp:me");
+        properties.setContentEncoding("text/UTF-8");
+        properties.setContentType("text");
+        properties.setCorrelationId("correlation-id");
+        properties.setAbsoluteExpiryTime(randomAbsoluteExpiry);
+        properties.setCreationTime(randomCreateTime);
+        properties.setGroupId("group-1");
+        properties.setGroupSequence(randomGroupSequence);
+        properties.setReplyToGroupId("group-1");
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Properties);
+
+            Properties decoded = (Properties) result;
+
+            assertNotNull(decoded.getAbsoluteExpiryTime());
+            assertEquals(Integer.toUnsignedLong(randomAbsoluteExpiry), decoded.getAbsoluteExpiryTime());
+            assertEquals("text/UTF-8", decoded.getContentEncoding());
+            assertEquals("text", decoded.getContentType());
+            assertEquals("correlation-id", decoded.getCorrelationId());
+            assertEquals(Integer.toUnsignedLong(randomCreateTime), decoded.getCreationTime());
+            assertEquals("group-1", decoded.getGroupId());
+            assertEquals(Integer.toUnsignedLong(randomGroupSequence), decoded.getGroupSequence());
+            assertEquals("ID:Message-1:1:1:0", decoded.getMessageId());
+            assertEquals("queue:temp:me", decoded.getReplyTo());
+            assertEquals("group-1", decoded.getReplyToGroupId());
+            assertEquals("help", decoded.getSubject());
+            assertEquals("queue:work", decoded.getTo());
+            assertTrue(decoded.getUserId() instanceof Binary);
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeWithMaxUnsignedValuesFromLongs() throws IOException {
+        doTestEncodeAndDecodeWithMaxUnsignedValuesFromLongs(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeWithMaxUnsignedValuesFromLongsFromStream() throws IOException {
+        doTestEncodeAndDecodeWithMaxUnsignedValuesFromLongs(true);
+    }
+
+    private void doTestEncodeAndDecodeWithMaxUnsignedValuesFromLongs(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+        final Properties properties = new Properties();
+
+        properties.setAbsoluteExpiryTime(UnsignedInteger.MAX_VALUE.longValue());
+        properties.setCreationTime(UnsignedInteger.MAX_VALUE.longValue());
+        properties.setGroupSequence(UnsignedInteger.MAX_VALUE.longValue());
+
+        encoder.writeObject(buffer, encoderState, properties);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Properties);
+
+        Properties decoded = (Properties) result;
+
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), decoded.getAbsoluteExpiryTime());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), decoded.getCreationTime());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), decoded.getGroupSequence());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Properties properties = new Properties();
+        properties.setAbsoluteExpiryTime(100);
+        properties.setContentEncoding("UTF8");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, properties);
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Properties.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Properties.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Properties.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Properties.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Properties.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Properties.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Properties[] array = new Properties[3];
+
+        array[0] = new Properties();
+        array[1] = new Properties();
+        array[2] = new Properties();
+
+        array[0].setAbsoluteExpiryTime(1);
+        array[1].setAbsoluteExpiryTime(2);
+        array[2].setAbsoluteExpiryTime(3);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Properties.class, result.getClass().getComponentType());
+
+        Properties[] resultArray = (Properties[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Properties);
+            assertEquals(array[i].getAbsoluteExpiryTime(), resultArray[i].getAbsoluteExpiryTime());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Properties.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(-1);  // Count, reads as negative as encoder treats these as signed ints.
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Properties.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ReceivedTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ReceivedTypeCodecTest.java
new file mode 100644
index 0000000..72dba89
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ReceivedTypeCodecTest.java
@@ -0,0 +1,469 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ReceivedTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ReceivedTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Received;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of Received types.
+ */
+public class ReceivedTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Received.class, new ReceivedTypeDecoder().getTypeClass());
+        assertEquals(Received.class, new ReceivedTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Received.DESCRIPTOR_CODE, new ReceivedTypeDecoder().getDescriptorCode());
+        assertEquals(Received.DESCRIPTOR_CODE, new ReceivedTypeEncoder().getDescriptorCode());
+        assertEquals(Received.DESCRIPTOR_SYMBOL, new ReceivedTypeDecoder().getDescriptorSymbol());
+        assertEquals(Received.DESCRIPTOR_SYMBOL, new ReceivedTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeRecieved() throws IOException {
+        doTestDecodeRecieved(false);
+    }
+
+    @Test
+    public void testDecodeRecievedFromStream() throws IOException {
+        doTestDecodeRecieved(true);
+    }
+
+    private void doTestDecodeRecieved(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Received value = new Received();
+        value.setSectionNumber(UnsignedInteger.ONE);
+        value.setSectionOffset(UnsignedLong.ZERO);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Received);
+    }
+
+    @Test
+    public void testDecodeReceivedWithList8() throws IOException {
+        doTestDecodeReceivedWithList8(false);
+    }
+
+    @Test
+    public void testDecodeReceivedWithList8FromStream() throws IOException {
+        doTestDecodeReceivedWithList8(true);
+    }
+
+    private void doTestDecodeReceivedWithList8(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Received.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 5);  // Size
+        buffer.writeByte((byte) 2);  // Count
+        buffer.writeByte(EncodingCodes.SMALLUINT);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(0);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Received);
+    }
+
+    @Test
+    public void testDecodeReceivedWithList32() throws IOException {
+        doTestDecodeReceivedWithList32(false);
+    }
+
+    @Test
+    public void testDecodeReceivedWithList32FromStream() throws IOException {
+        doTestDecodeReceivedWithList32(true);
+    }
+
+    public void doTestDecodeReceivedWithList32(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Received.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt(8);  // Size
+        buffer.writeInt(2);  // Count
+        buffer.writeByte(EncodingCodes.SMALLUINT);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(0);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Received);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Received received = new Received();
+        received.setSectionNumber(UnsignedInteger.ONE);
+        received.setSectionOffset(UnsignedLong.ZERO);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, received);
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Received.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Received.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Received.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {
+            }
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {
+            }
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Received.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Received.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Received.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Received[] array = new Received[3];
+
+        array[0] = new Received();
+        array[1] = new Received();
+        array[2] = new Received();
+
+        array[0].setSectionNumber(UnsignedInteger.ONE).setSectionOffset(UnsignedLong.ZERO);
+        array[1].setSectionNumber(UnsignedInteger.ONE).setSectionOffset(UnsignedLong.ZERO);
+        array[2].setSectionNumber(UnsignedInteger.ZERO).setSectionOffset(UnsignedLong.ZERO);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Received.class, result.getClass().getComponentType());
+
+        Received[] resultArray = (Received[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Received);
+            assertEquals(array[i].getSectionNumber(), resultArray[i].getSectionNumber());
+            assertEquals(array[i].getSectionOffset(), resultArray[i].getSectionOffset());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Received.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(-1);  // Count, reads as negative as encoder treats these as signed ints.
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Received.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/RejectedTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/RejectedTypeCodecTest.java
new file mode 100644
index 0000000..88a2129
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/RejectedTypeCodecTest.java
@@ -0,0 +1,492 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.RejectedTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.RejectedTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.transactions.TransactionErrors;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of Rejected types.
+ */
+public class RejectedTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Rejected.class, new RejectedTypeDecoder().getTypeClass());
+        assertEquals(Rejected.class, new RejectedTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Rejected.DESCRIPTOR_CODE, new RejectedTypeDecoder().getDescriptorCode());
+        assertEquals(Rejected.DESCRIPTOR_CODE, new RejectedTypeEncoder().getDescriptorCode());
+        assertEquals(Rejected.DESCRIPTOR_SYMBOL, new RejectedTypeDecoder().getDescriptorSymbol());
+        assertEquals(Rejected.DESCRIPTOR_SYMBOL, new RejectedTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeRejected() throws IOException {
+        doTestDecodeRejected(false);
+    }
+
+    @Test
+    public void testDecodeRejectedFromStream() throws IOException {
+        doTestDecodeRejected(true);
+    }
+
+    private void doTestDecodeRejected(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Rejected value = new Rejected();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Rejected);
+    }
+
+    @Test
+    public void testDecodeRejectedWithErrorCondition() throws IOException {
+        testDecodeRejectedWithErrorCondition(false);
+    }
+
+    @Test
+    public void testDecodeRejectedWithErrorConditionFromStream() throws IOException {
+        testDecodeRejectedWithErrorCondition(true);
+    }
+
+    private void testDecodeRejectedWithErrorCondition(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ErrorCondition error = new ErrorCondition(AmqpError.DECODE_ERROR, "invalid");
+
+        Rejected value = new Rejected();
+        value.setError(error);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Rejected);
+
+        value = (Rejected) result;
+        assertNotNull(value.getError());
+        assertEquals(error, value.getError());
+    }
+
+    @Test
+    public void testDecodeRejectedWithList8() throws IOException {
+        doTestDecodeRejectedWithList8(false);
+    }
+
+    @Test
+    public void testDecodeRejectedWithList8FromStream() throws IOException {
+        doTestDecodeRejectedWithList8(true);
+    }
+
+    private void doTestDecodeRejectedWithList8(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Rejected.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Rejected);
+    }
+
+    @Test
+    public void testDecodeRejectedWithList32() throws IOException {
+        testDecodeRejectedWithList32(false);
+    }
+
+    @Test
+    public void testDecodeRejectedWithList32FromStream() throws IOException {
+        testDecodeRejectedWithList32(true);
+    }
+
+    private void testDecodeRejectedWithList32(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Rejected.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Rejected);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Rejected rejected = new Rejected();
+        rejected.setError(new ErrorCondition(TransactionErrors.TRANSACTION_ROLLBACK, "Failure"));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, rejected);
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Rejected.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Rejected.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Rejected.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Rejected.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Rejected.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Rejected.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Rejected[] array = new Rejected[3];
+
+        array[0] = new Rejected();
+        array[1] = new Rejected();
+        array[2] = new Rejected();
+
+        array[0].setError(new ErrorCondition(AmqpError.DECODE_ERROR, "1"));
+        array[0].setError(new ErrorCondition(AmqpError.FRAME_SIZE_TOO_SMALL, "2"));
+        array[0].setError(new ErrorCondition(AmqpError.INVALID_FIELD, "3"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Rejected.class, result.getClass().getComponentType());
+
+        Rejected[] resultArray = (Rejected[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Rejected);
+            assertEquals(array[i].getError(), resultArray[i].getError());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Rejected.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(-1);  // Count, reads as negative as encoder treats these as signed ints.
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Rejected.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ReleasedTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ReleasedTypeCodecTest.java
new file mode 100644
index 0000000..4e7e835
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/ReleasedTypeCodecTest.java
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.ReleasedTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.ReleasedTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test codec handling of Released types.
+ */
+public class ReleasedTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Released.class, new ReleasedTypeDecoder().getTypeClass());
+        assertEquals(Released.class, new ReleasedTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Released.DESCRIPTOR_CODE, new ReleasedTypeDecoder().getDescriptorCode());
+        assertEquals(Released.DESCRIPTOR_CODE, new ReleasedTypeEncoder().getDescriptorCode());
+        assertEquals(Released.DESCRIPTOR_SYMBOL, new ReleasedTypeDecoder().getDescriptorSymbol());
+        assertEquals(Released.DESCRIPTOR_SYMBOL, new ReleasedTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testDecodeReleased() throws IOException {
+        doTestDecodeReleased(false);
+    }
+
+    @Test
+    public void testDecodeReleasedFromStream() throws IOException {
+        doTestDecodeReleased(true);
+    }
+
+    private void doTestDecodeReleased(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Released value = Released.getInstance();
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Released);
+    }
+
+    @Test
+    public void testDecodeReleasedWithList8() throws IOException {
+        doTestDecodeReleasedWithList8(false);
+    }
+
+    @Test
+    public void testDecodeReleasedWithList8FromStream() throws IOException {
+        doTestDecodeReleasedWithList8(true);
+    }
+
+    private void doTestDecodeReleasedWithList8(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Released.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte((byte) 0);  // Size
+        buffer.writeByte((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Released);
+    }
+
+    @Test
+    public void testDecodeReleasedWithList32() throws IOException {
+        doTestDecodeReleasedWithList32(false);
+    }
+
+    @Test
+    public void testDecodeReleasedWithList32FromStream() throws IOException {
+        doTestDecodeReleasedWithList32(true);
+    }
+
+    private void doTestDecodeReleasedWithList32(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Released.DESCRIPTOR_CODE.byteValue());
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt((byte) 0);  // Size
+        buffer.writeInt((byte) 0);  // Count
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Released);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, Released.getInstance());
+        }
+
+        encoder.writeObject(buffer, encoderState, new Modified());
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Released.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Released.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Modified);
+        Modified modified = (Modified) result;
+        assertFalse(modified.isUndeliverableHere());
+        assertFalse(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Released.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Released.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Released.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Released.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Released[] array = new Released[3];
+
+        array[0] = Released.getInstance();
+        array[1] = Released.getInstance();
+        array[2] = Released.getInstance();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Released.class, result.getClass().getComponentType());
+
+        Released[] resultArray = (Released[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Released);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/SourceTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/SourceTypeCodecTest.java
new file mode 100644
index 0000000..4d6b6d2
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/SourceTypeCodecTest.java
@@ -0,0 +1,455 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.SourceTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.SourceTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Source serialization
+ */
+public class SourceTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Source.class, new SourceTypeDecoder().getTypeClass());
+        assertEquals(Source.class, new SourceTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Source.DESCRIPTOR_CODE, new SourceTypeDecoder().getDescriptorCode());
+        assertEquals(Source.DESCRIPTOR_CODE, new SourceTypeEncoder().getDescriptorCode());
+        assertEquals(Source.DESCRIPTOR_SYMBOL, new SourceTypeDecoder().getDescriptorSymbol());
+        assertEquals(Source.DESCRIPTOR_SYMBOL, new SourceTypeEncoder().getDescriptorSymbol());
+    }
+
+   @Test
+   public void testEncodeDecodeSourceType() throws Exception {
+       doTestEncodeDecodeSourceType(false);
+   }
+
+   @Test
+   public void testEncodeDecodeSourceTypeFromStream() throws Exception {
+       doTestEncodeDecodeSourceType(true);
+   }
+
+   private void doTestEncodeDecodeSourceType(boolean fromStream) throws Exception {
+      final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+      final InputStream stream = new ProtonBufferInputStream(buffer);
+
+      Source value = new Source();
+      value.setAddress("test");
+      value.setDurable(TerminusDurability.UNSETTLED_STATE);
+      value.setTimeout(UnsignedInteger.MAX_VALUE);
+
+      encoder.writeObject(buffer, encoderState, value);
+
+      final Source result;
+      if (fromStream) {
+          result = streamDecoder.readObject(stream, streamDecoderState, Source.class);
+      } else {
+          result = decoder.readObject(buffer, decoderState, Source.class);
+      }
+
+      assertEquals("test", result.getAddress());
+      assertEquals(TerminusDurability.UNSETTLED_STATE, result.getDurable());
+      assertEquals(UnsignedInteger.MAX_VALUE, result.getTimeout());
+   }
+
+   @Test
+   public void testFullyPopulatedSource() throws Exception {
+       doTestFullyPopulatedSource(false);
+   }
+
+   @Test
+   public void testFullyPopulatedSourceFromStream() throws Exception {
+       doTestFullyPopulatedSource(true);
+   }
+
+   private void doTestFullyPopulatedSource(boolean fromStream) throws Exception {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+      Map<Symbol, Object> nodeProperties = new LinkedHashMap<>();
+      nodeProperties.put(Symbol.valueOf("property-1"), "value-1");
+      nodeProperties.put(Symbol.valueOf("property-2"), "value-2");
+      nodeProperties.put(Symbol.valueOf("property-3"), "value-3");
+
+      Map<Symbol, Object> filters = new LinkedHashMap<>();
+      nodeProperties.put(Symbol.valueOf("filter-1"), "value-1");
+      nodeProperties.put(Symbol.valueOf("filter-2"), "value-2");
+      nodeProperties.put(Symbol.valueOf("filter-3"), "value-3");
+
+      Source value = new Source();
+      value.setAddress("test");
+      value.setDurable(TerminusDurability.UNSETTLED_STATE);
+      value.setExpiryPolicy(TerminusExpiryPolicy.SESSION_END);
+      value.setTimeout(UnsignedInteger.valueOf(255));
+      value.setDynamic(true);
+      value.setDynamicNodeProperties(nodeProperties);
+      value.setDistributionMode(Symbol.valueOf("mode"));
+      value.setFilter(filters);
+      value.setDefaultOutcome(Released.getInstance());
+      value.setOutcomes(new Symbol[] {Symbol.valueOf("ACCEPTED"), Symbol.valueOf("REJECTED")});
+      value.setCapabilities(new Symbol[] {Symbol.valueOf("RELEASED"), Symbol.valueOf("MODIFIED")});
+
+      encoder.writeObject(buffer, encoderState, value);
+
+      final Source result;
+      if (fromStream) {
+          result = streamDecoder.readObject(stream, streamDecoderState, Source.class);
+      } else {
+          result = decoder.readObject(buffer, decoderState, Source.class);
+      }
+
+      assertEquals("test", result.getAddress());
+      assertEquals(TerminusDurability.UNSETTLED_STATE, result.getDurable());
+      assertEquals(TerminusExpiryPolicy.SESSION_END, result.getExpiryPolicy());
+      assertEquals(UnsignedInteger.valueOf(255), result.getTimeout());
+      assertEquals(true, result.isDynamic());
+      assertEquals(nodeProperties, result.getDynamicNodeProperties());
+      assertEquals(Symbol.valueOf("mode"), result.getDistributionMode());
+      assertEquals(filters, result.getFilter());
+      assertEquals(Released.getInstance(), result.getDefaultOutcome());
+
+      assertArrayEquals(new Symbol[] {Symbol.valueOf("ACCEPTED"), Symbol.valueOf("REJECTED")}, result.getOutcomes());
+      assertArrayEquals(new Symbol[] {Symbol.valueOf("RELEASED"), Symbol.valueOf("MODIFIED")}, result.getCapabilities());
+   }
+
+   @Test
+   public void testSkipValue() throws IOException {
+       doTestSkipValue(false);
+   }
+
+   @Test
+   public void testSkipValueFromStream() throws IOException {
+       doTestSkipValue(true);
+   }
+
+   private void doTestSkipValue(boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Source source = new Source();
+       source.setAddress("address");
+       source.setCapabilities(Symbol.valueOf("QUEUE"));
+
+       for (int i = 0; i < 10; ++i) {
+           encoder.writeObject(buffer, encoderState, source);
+       }
+
+       encoder.writeObject(buffer, encoderState, new Modified());
+
+       for (int i = 0; i < 10; ++i) {
+           if (fromStream) {
+               StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+               assertEquals(Source.class, typeDecoder.getTypeClass());
+               typeDecoder.skipValue(stream, streamDecoderState);
+           } else {
+               TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+               assertEquals(Source.class, typeDecoder.getTypeClass());
+               typeDecoder.skipValue(buffer, decoderState);
+           }
+       }
+
+       final Object result;
+       if (fromStream) {
+           result = streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = decoder.readObject(buffer, decoderState);
+       }
+
+       assertNotNull(result);
+       assertTrue(result instanceof Modified);
+       Modified modified = (Modified) result;
+       assertFalse(modified.isUndeliverableHere());
+       assertFalse(modified.isDeliveryFailed());
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap32Type() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap8Type() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+   }
+
+   private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Source.DESCRIPTOR_CODE.byteValue());
+       if (mapType == EncodingCodes.MAP32) {
+           buffer.writeByte(EncodingCodes.MAP32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.MAP8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid encoding");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid encoding");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap32Type() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap8Type() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+   }
+
+   private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Source.DESCRIPTOR_CODE.byteValue());
+       if (mapType == EncodingCodes.MAP32) {
+           buffer.writeByte(EncodingCodes.MAP32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.MAP8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       }
+
+       if (fromStream) {
+           StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+           assertEquals(Source.class, typeDecoder.getTypeClass());
+
+           try {
+               typeDecoder.skipValue(stream, streamDecoderState);
+               fail("Should not be able to skip type with invalid encoding");
+           } catch (DecodeException ex) {}
+       } else {
+           TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+           assertEquals(Source.class, typeDecoder.getTypeClass());
+
+           try {
+               typeDecoder.skipValue(buffer, decoderState);
+               fail("Should not be able to skip type with invalid encoding");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testEncodeDecodeArray() throws IOException {
+       doTestEncodeDecodeArray(false);
+   }
+
+   @Test
+   public void testEncodeDecodeArrayFromStream() throws IOException {
+       doTestEncodeDecodeArray(true);
+   }
+
+   private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Source[] array = new Source[3];
+
+       array[0] = new Source();
+       array[1] = new Source();
+       array[2] = new Source();
+
+       array[0].setAddress("test-1").setDynamic(true).setDefaultOutcome(Accepted.getInstance());
+       array[1].setAddress("test-2").setDynamic(false).setDefaultOutcome(Released.getInstance());
+       array[2].setAddress("test-3").setDynamic(true).setDefaultOutcome(Accepted.getInstance());
+
+       encoder.writeObject(buffer, encoderState, array);
+
+       final Object result;
+       if (fromStream) {
+           result = streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = decoder.readObject(buffer, decoderState);
+       }
+
+       assertTrue(result.getClass().isArray());
+       assertEquals(Source.class, result.getClass().getComponentType());
+
+       Source[] resultArray = (Source[]) result;
+
+       for (int i = 0; i < resultArray.length; ++i) {
+           assertNotNull(resultArray[i]);
+           assertTrue(resultArray[i] instanceof Source);
+           assertEquals(array[i].getAddress(), resultArray[i].getAddress());
+           assertEquals(array[i].isDynamic(), resultArray[i].isDynamic());
+           assertEquals(array[i].getDefaultOutcome(), resultArray[i].getDefaultOutcome());
+       }
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+   }
+
+   private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Source.DESCRIPTOR_CODE.byteValue());
+       if (listType == EncodingCodes.LIST32) {
+           buffer.writeByte(EncodingCodes.LIST32);
+           buffer.writeInt(128);  // Size
+           buffer.writeInt(-1);  // Count, reads as negative as encoder treats these as signed ints.
+       } else if (listType == EncodingCodes.LIST8) {
+           buffer.writeByte(EncodingCodes.LIST8);
+           buffer.writeByte((byte) 128);  // Size
+           buffer.writeByte((byte) 0xFF);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList8() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList32() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+   }
+
+   private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Source.DESCRIPTOR_CODE.byteValue());
+       if (listType == EncodingCodes.LIST32) {
+           buffer.writeByte(EncodingCodes.LIST32);
+           buffer.writeInt(128);  // Size
+           buffer.writeInt(127);  // Count
+       } else if (listType == EncodingCodes.LIST8) {
+           buffer.writeByte(EncodingCodes.LIST8);
+           buffer.writeByte((byte) 128);  // Size
+           buffer.writeByte((byte) 127);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       }
+   }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/TargetTypeCodeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/TargetTypeCodeTest.java
new file mode 100644
index 0000000..8e5da85
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/messaging/TargetTypeCodeTest.java
@@ -0,0 +1,450 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.messaging.TargetTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.messaging.TargetTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.TerminusDurability;
+import org.apache.qpid.protonj2.types.messaging.TerminusExpiryPolicy;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Source serialization
+ */
+public class TargetTypeCodeTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Target.class, new TargetTypeDecoder().getTypeClass());
+        assertEquals(Target.class, new TargetTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Target.DESCRIPTOR_CODE, new TargetTypeDecoder().getDescriptorCode());
+        assertEquals(Target.DESCRIPTOR_CODE, new TargetTypeEncoder().getDescriptorCode());
+        assertEquals(Target.DESCRIPTOR_SYMBOL, new TargetTypeDecoder().getDescriptorSymbol());
+        assertEquals(Target.DESCRIPTOR_SYMBOL, new TargetTypeEncoder().getDescriptorSymbol());
+    }
+
+   @Test
+   public void testEncodeDecodeOfTarget() throws Exception {
+       testEncodeDecodeOfTarget(false);
+   }
+
+   @Test
+   public void testEncodeDecodeOfTargetFromStream() throws Exception {
+       testEncodeDecodeOfTarget(true);
+   }
+
+   private void testEncodeDecodeOfTarget(boolean fromStream) throws Exception {
+      ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+      InputStream stream = new ProtonBufferInputStream(buffer);
+
+      Target value = new Target();
+      value.setAddress("test");
+      value.setDurable(TerminusDurability.UNSETTLED_STATE);
+      value.setTimeout(UnsignedInteger.ZERO);
+
+      encoder.writeObject(buffer, encoderState, value);
+
+      final Target result;
+      if (fromStream) {
+          result = streamDecoder.readObject(stream, streamDecoderState, Target.class);
+      } else {
+          result = decoder.readObject(buffer, decoderState, Target.class);
+      }
+
+      assertEquals("test", result.getAddress());
+      assertEquals(TerminusDurability.UNSETTLED_STATE, result.getDurable());
+      assertEquals(UnsignedInteger.ZERO, result.getTimeout());
+   }
+
+   @Test
+   public void testFullyPopulatedTarget() throws Exception {
+       testFullyPopulatedTarget(false);
+   }
+
+   @Test
+   public void testFullyPopulatedTargetFromStream() throws Exception {
+       testFullyPopulatedTarget(true);
+   }
+
+   private void testFullyPopulatedTarget(boolean fromStream) throws Exception {
+      ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+      InputStream stream = new ProtonBufferInputStream(buffer);
+
+      Map<Symbol, Object> nodeProperties = new LinkedHashMap<>();
+      nodeProperties.put(Symbol.valueOf("property-1"), "value-1");
+      nodeProperties.put(Symbol.valueOf("property-2"), "value-2");
+      nodeProperties.put(Symbol.valueOf("property-3"), "value-3");
+
+      Target value = new Target();
+      value.setAddress("test");
+      value.setDurable(TerminusDurability.UNSETTLED_STATE);
+      value.setExpiryPolicy(TerminusExpiryPolicy.CONNECTION_CLOSE);
+      value.setTimeout(UnsignedInteger.valueOf(1024));
+      value.setDynamic(false);
+      value.setCapabilities(new Symbol[] {Symbol.valueOf("RELEASED"), Symbol.valueOf("MODIFIED")});
+      value.setDynamicNodeProperties(nodeProperties);
+
+      encoder.writeObject(buffer, encoderState, value);
+
+      final Target result;
+      if (fromStream) {
+          result = streamDecoder.readObject(stream, streamDecoderState, Target.class);
+      } else {
+          result = decoder.readObject(buffer, decoderState, Target.class);
+      }
+
+      assertEquals("test", result.getAddress());
+      assertEquals(TerminusDurability.UNSETTLED_STATE, result.getDurable());
+      assertEquals(TerminusExpiryPolicy.CONNECTION_CLOSE, result.getExpiryPolicy());
+      assertEquals(UnsignedInteger.valueOf(1024), result.getTimeout());
+      assertEquals(false, result.isDynamic());
+      assertEquals(nodeProperties, result.getDynamicNodeProperties());
+      assertArrayEquals(new Symbol[] {Symbol.valueOf("RELEASED"), Symbol.valueOf("MODIFIED")}, result.getCapabilities());
+   }
+
+   @Test
+   public void testSkipValue() throws IOException {
+       testSkipValue(false);
+   }
+
+   @Test
+   public void testSkipValueFromStream() throws IOException {
+       testSkipValue(true);
+   }
+
+   private void testSkipValue(boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Target target = new Target();
+       target.setAddress("address");
+       target.setDurable(TerminusDurability.CONFIGURATION);
+       target.setCapabilities(Symbol.valueOf("QUEUE"));
+
+       for (int i = 0; i < 10; ++i) {
+           encoder.writeObject(buffer, encoderState, target);
+       }
+
+       encoder.writeObject(buffer, encoderState, new Modified());
+
+       for (int i = 0; i < 10; ++i) {
+           if (fromStream) {
+               StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+               assertEquals(Target.class, typeDecoder.getTypeClass());
+               typeDecoder.skipValue(stream, streamDecoderState);
+           } else {
+               TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+               assertEquals(Target.class, typeDecoder.getTypeClass());
+               typeDecoder.skipValue(buffer, decoderState);
+           }
+       }
+
+       final Object result;
+       if (fromStream) {
+           result = streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = decoder.readObject(buffer, decoderState);
+       }
+
+       assertNotNull(result);
+       assertTrue(result instanceof Modified);
+       Modified modified = (Modified) result;
+       assertFalse(modified.isUndeliverableHere());
+       assertFalse(modified.isDeliveryFailed());
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap32Type() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap8Type() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap32TypeFromStream() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+   }
+
+   private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Target.DESCRIPTOR_CODE.byteValue());
+       if (mapType == EncodingCodes.MAP32) {
+           buffer.writeByte(EncodingCodes.MAP32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.MAP8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid encoding");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid encoding");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap32Type() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap8Type() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+   }
+
+   private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Target.DESCRIPTOR_CODE.byteValue());
+       if (mapType == EncodingCodes.MAP32) {
+           buffer.writeByte(EncodingCodes.MAP32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.MAP8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       }
+
+       if (fromStream) {
+           StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+           assertEquals(Target.class, typeDecoder.getTypeClass());
+
+           try {
+               typeDecoder.skipValue(stream, streamDecoderState);
+               fail("Should not be able to skip type with invalid encoding");
+           } catch (DecodeException ex) {}
+       } else {
+           TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+           assertEquals(Target.class, typeDecoder.getTypeClass());
+
+           try {
+               typeDecoder.skipValue(buffer, decoderState);
+               fail("Should not be able to skip type with invalid encoding");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testEncodeDecodeArray() throws IOException {
+       testEncodeDecodeArray(false);
+   }
+
+   @Test
+   public void testEncodeDecodeArrayFromStream() throws IOException {
+       testEncodeDecodeArray(true);
+   }
+
+   private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Target[] array = new Target[3];
+
+       array[0] = new Target();
+       array[1] = new Target();
+       array[2] = new Target();
+
+       array[0].setAddress("test-1").setDynamic(true).setDurable(TerminusDurability.CONFIGURATION);
+       array[1].setAddress("test-2").setDynamic(true).setDurable(TerminusDurability.NONE);
+       array[2].setAddress("test-3").setDynamic(true).setDurable(TerminusDurability.UNSETTLED_STATE);
+
+       encoder.writeObject(buffer, encoderState, array);
+
+       final Object result;
+       if (fromStream) {
+           result = streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = decoder.readObject(buffer, decoderState);
+       }
+
+       assertTrue(result.getClass().isArray());
+       assertEquals(Target.class, result.getClass().getComponentType());
+
+       Target[] resultArray = (Target[]) result;
+
+       for (int i = 0; i < resultArray.length; ++i) {
+           assertNotNull(resultArray[i]);
+           assertTrue(resultArray[i] instanceof Target);
+           assertEquals(array[i].getAddress(), resultArray[i].getAddress());
+           assertEquals(array[i].isDynamic(), resultArray[i].isDynamic());
+           assertEquals(array[i].getDurable(), resultArray[i].getDurable());
+       }
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+   }
+
+   private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Target.DESCRIPTOR_CODE.byteValue());
+       if (listType == EncodingCodes.LIST32) {
+           buffer.writeByte(EncodingCodes.LIST32);
+           buffer.writeInt(128);  // Size
+           buffer.writeInt(-1);  // Count, reads as negative as encoder treats these as signed ints.
+       } else if (listType == EncodingCodes.LIST8) {
+           buffer.writeByte(EncodingCodes.LIST8);
+           buffer.writeByte((byte) 128);  // Size
+           buffer.writeByte((byte) 0xFF);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList8() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList32() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+   }
+
+   private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Target.DESCRIPTOR_CODE.byteValue());
+       if (listType == EncodingCodes.LIST32) {
+           buffer.writeByte(EncodingCodes.LIST32);
+           buffer.writeInt(128);  // Size
+           buffer.writeInt(127);  // Count
+       } else if (listType == EncodingCodes.LIST8) {
+           buffer.writeByte(EncodingCodes.LIST8);
+           buffer.writeByte((byte) 128);  // Size
+           buffer.writeByte((byte) 127);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       }
+   }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ArrayTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ArrayTypeCodecTest.java
new file mode 100644
index 0000000..6031f8c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ArrayTypeCodecTest.java
@@ -0,0 +1,1470 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test decoding of AMQP Array types
+ */
+public class ArrayTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testWriteOfZeroSizedGenericArrayFails() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Object[] source = new Object[0];
+
+        try {
+            encoder.writeArray(buffer, encoderState, source);
+            fail("Should reject attempt to write zero sized array of unknown type.");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testWriteOfGenericArrayOfObjectsFails() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Object[] source = new Object[2];
+
+        source[0] = new Object();
+        source[1] = new Object();
+
+        try {
+            encoder.writeArray(buffer, encoderState, source);
+            fail("Should reject attempt to write array of unknown type.");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testArrayOfArraysOfMixedTypes() throws IOException {
+        doTestArrayOfArraysOfMixedTypes(false);
+    }
+
+    @Test
+    public void testArrayOfArraysOfMixedTypesFromStream() throws IOException {
+        doTestArrayOfArraysOfMixedTypes(true);
+    }
+
+    private void doTestArrayOfArraysOfMixedTypes(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Object[][] source = new Object[2][size];
+        for (int i = 0; i < size; ++i) {
+            source[0][i] = Short.valueOf((short) i);
+            source[1][i] = Integer.valueOf(i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Object[] resultArray = (Object[]) result;
+
+        assertNotNull(resultArray);
+        assertEquals(2, resultArray.length);
+
+        assertTrue(resultArray[0].getClass().isArray());
+        assertTrue(resultArray[1].getClass().isArray());
+    }
+
+    @Test
+    public void testArrayOfArraysOfArraysOfShortTypes() throws IOException {
+        testArrayOfArraysOfArraysOfShortTypes(false);
+    }
+
+    @Test
+    public void testArrayOfArraysOfArraysOfShortTypesFromStream() throws IOException {
+        testArrayOfArraysOfArraysOfShortTypes(true);
+    }
+
+    private void testArrayOfArraysOfArraysOfShortTypes(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Object[][][] source = new Object[2][2][size];
+        for (int i = 0; i < source.length; ++i) {
+            for (int j = 0; j < source[i].length; ++j) {
+                for (int k = 0; k < source[i][j].length; ++k) {
+                    source[i][j][k] = (short) k;
+                }
+             }
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Object[] resultArray = (Object[]) result;
+
+        assertNotNull(resultArray);
+        assertEquals(2, resultArray.length);
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertTrue(resultArray[i].getClass().isArray());
+
+            Object[] dimension2 = (Object[]) resultArray[i];
+            assertEquals(2, dimension2.length);
+
+            for (int j = 0; j < dimension2.length; ++j) {
+                short[] dimension3 = (short[]) dimension2[j];
+                assertEquals(size, dimension3.length);
+
+                for (int k = 0; k < dimension3.length; ++k) {
+                    assertEquals(source[i][j][k], dimension3[k]);
+                }
+             }
+        }
+    }
+
+    @Test
+    public void testWriteArrayOfArraysStrings() throws IOException {
+        testWriteArrayOfArraysStrings(false);
+    }
+
+    @Test
+    public void testWriteArrayOfArraysStringsFromStream() throws IOException {
+        testWriteArrayOfArraysStrings(true);
+    }
+
+    private void testWriteArrayOfArraysStrings(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        String[][] stringArray = new String[2][1];
+
+        stringArray[0][0] = "short-string";
+        stringArray[1][0] = "long-string-entry:" + UUID.randomUUID().toString() + "," +
+                                                   UUID.randomUUID().toString() + "," +
+                                                   UUID.randomUUID().toString() + "," +
+                                                   UUID.randomUUID().toString() + "," +
+                                                   UUID.randomUUID().toString() + "," +
+                                                   UUID.randomUUID().toString() + "," +
+                                                   UUID.randomUUID().toString();
+
+        encoder.writeArray(buffer, encoderState, stringArray);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Object[] array = (Object[]) result;
+        assertEquals(2, array.length);
+
+        assertTrue(array[0] instanceof String[]);
+        assertTrue(array[1] instanceof String[]);
+
+        String[] element1Array = (String[]) array[0];
+        String[] element2Array = (String[]) array[1];
+
+        assertEquals(1, element1Array.length);
+        assertEquals(1, element2Array.length);
+
+        assertEquals(stringArray[0][0], element1Array[0]);
+        assertEquals(stringArray[1][0], element2Array[0]);
+    }
+
+    @Test
+    public void testEncodeArrayWithNullEntriesMatchesLegacy() throws Exception {
+        Symbol[] input1 = new Symbol[] { null };
+        Symbol[] input2 = new Symbol[] { AmqpError.DECODE_ERROR, null };
+
+        try {
+            legacyCodec.encodeUsingLegacyEncoder(input1);
+            fail("Should fail as no type encoder can be deduced");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+
+        try {
+            ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+            encoder.writeObject(buffer, encoderState, input1);
+            fail("Should fail as no type encoder can be deduced");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+
+        try {
+            legacyCodec.encodeUsingLegacyEncoder(input2);
+            fail("Should fail as no type encoder cannot handle null elements");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+
+        try {
+            ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+            encoder.writeObject(buffer, encoderState, input2);
+            fail("Should fail as no type encoder cannot handle null elements");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testEncodeStringArrayWithNewCodecAndDecodeWithOldCodec() throws Exception {
+        String[] input = new String[] { "test", "legacy", "codec" };
+
+        ProtonBuffer buffer = legacyCodec.encodeUsingLegacyEncoder(input);
+
+        assertNotNull(buffer);
+
+        Object decoded = decoder.readObject(buffer, decoderState);
+        assertNotNull(decoded);
+        assertTrue(decoded.getClass().isArray());
+        assertEquals(String.class, decoded.getClass().getComponentType());
+        String[] result = (String[]) decoded;
+        assertArrayEquals(input, result);
+    }
+
+    @Test
+    public void testEncodeStringArrayUsingNewCodecAndDecodeWithLegacyCodec() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        String[] input = new String[] { "test", "legacy", "codec" };
+
+        encoder.writeObject(buffer, encoderState, input);
+        Object decoded = legacyCodec.decodeLegacyType(buffer);
+
+        assertNotNull(decoded);
+        assertTrue(decoded.getClass().isArray());
+        assertEquals(String.class, decoded.getClass().getComponentType());
+        String[] result = (String[]) decoded;
+        assertArrayEquals(input, result);
+    }
+
+    @Test
+    public void testEncodeAndDecodeArrayOfListsUsingReadMultiple() throws Exception {
+        testEncodeAndDecodeArrayOfListsUsingReadMultiple(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeArrayOfListsUsingReadMultipleFromStream() throws Exception {
+        testEncodeAndDecodeArrayOfListsUsingReadMultiple(true);
+    }
+
+    private void testEncodeAndDecodeArrayOfListsUsingReadMultiple(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        @SuppressWarnings("rawtypes")
+        List[] lists = new List[3];
+
+        ArrayList<String> content1 = new ArrayList<>();
+        ArrayList<String> content2 = new ArrayList<>();
+        ArrayList<String> content3 = new ArrayList<>();
+
+        content1.add("test-1");
+        content2.add("test-2");
+        content3.add("test-3");
+
+        lists[0] = content1;
+        lists[1] = content2;
+        lists[2] = content3;
+
+        encoder.writeObject(buffer, encoderState, lists);
+
+        @SuppressWarnings("rawtypes")
+        final List[] decoded;
+        if (fromStream) {
+            decoded = streamDecoder.readMultiple(stream, streamDecoderState, List.class);
+        } else {
+            decoded = decoder.readMultiple(buffer, decoderState, List.class);
+        }
+
+        assertNotNull(decoded);
+        assertTrue(decoded.getClass().isArray());
+        assertEquals(List.class, decoded.getClass().getComponentType());
+        assertArrayEquals(lists, decoded);
+    }
+
+    @Test
+    public void testEncodeAndDecodeArrayOfMapsUsingReadMultiple() throws Exception {
+        testEncodeAndDecodeArrayOfMapsUsingReadMultiple(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeArrayOfMapsUsingReadMultipleFromStream() throws Exception {
+        testEncodeAndDecodeArrayOfMapsUsingReadMultiple(true);
+    }
+
+    private void testEncodeAndDecodeArrayOfMapsUsingReadMultiple(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        @SuppressWarnings("rawtypes")
+        Map[] maps = new Map[3];
+
+        Map<String, Object> content1 = new LinkedHashMap<>();
+        Map<String, Object> content2 = new LinkedHashMap<>();
+        Map<String, Object> content3 = new LinkedHashMap<>();
+
+        content1.put("test-1", UUID.randomUUID());
+        content2.put("test-2", "String");
+        content3.put("test-3", Boolean.FALSE);
+
+        maps[0] = content1;
+        maps[1] = content2;
+        maps[2] = content3;
+
+        encoder.writeObject(buffer, encoderState, maps);
+
+        @SuppressWarnings("rawtypes")
+        final Map[] decoded;
+        if (fromStream) {
+            decoded = streamDecoder.readMultiple(stream, streamDecoderState, Map.class);
+        } else {
+            decoded = decoder.readMultiple(buffer, decoderState, Map.class);
+        }
+
+        assertNotNull(decoded);
+        assertTrue(decoded.getClass().isArray());
+        assertEquals(Map.class, decoded.getClass().getComponentType());
+        assertArrayEquals(maps, decoded);
+    }
+
+    @Test
+    public void testEncodeDecodeBooleanArray100() throws Throwable {
+        // boolean array8 less than 128 bytes
+        doEncodeDecodeBooleanArrayTestImpl(100, false);
+    }
+
+    @Test
+    public void testEncodeDecodeBooleanArray192() throws Throwable {
+        // boolean array8 greater than 128 bytes
+        doEncodeDecodeBooleanArrayTestImpl(192, false);
+    }
+
+    @Test
+    public void testEncodeDecodeBooleanArray384() throws Throwable {
+        // boolean array32
+        doEncodeDecodeBooleanArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeBooleanArray100FS() throws Throwable {
+        // boolean array8 less than 128 bytes
+        doEncodeDecodeBooleanArrayTestImpl(100, true);
+    }
+
+    @Test
+    public void testEncodeDecodeBooleanArray192FS() throws Throwable {
+        // boolean array8 greater than 128 bytes
+        doEncodeDecodeBooleanArrayTestImpl(192, true);
+    }
+
+    @Test
+    public void testEncodeDecodeBooleanArray384FS() throws Throwable {
+        // boolean array32
+        doEncodeDecodeBooleanArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeBooleanArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        boolean[] source = createPayloadArrayBooleans(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 254 ? 1 : 4; // less than 254 and not 256, since we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int arrayPayloadSize =  encodingWidth + 1 + count; // variable width for element count + byte type descriptor + number of elements
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x56); // 'boolean' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                byte booleanCode = (byte) (source[i] ? 0x01 : 0x00); //  0x01 true, 0x00 false.
+                expectedEncodingWrapper.writeByte(booleanCode);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(boolean.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (boolean[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static boolean[] createPayloadArrayBooleans(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        boolean[] payload = new boolean[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = rand.nextBoolean();
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray100() throws Throwable {
+        // byte array8 less than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(100, false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray192() throws Throwable {
+        // byte array8 greater than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(192, false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray254() throws Throwable {
+        // byte array8 greater than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(254, false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray255() throws Throwable {
+        // byte array8 greater than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(255, false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray384() throws Throwable {
+        // byte array32
+        doEncodeDecodeByteArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray100FS() throws Throwable {
+        // byte array8 less than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(100, true);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray192FS() throws Throwable {
+        // byte array8 greater than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(192, true);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray254FS() throws Throwable {
+        // byte array8 greater than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(254, true);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray255FS() throws Throwable {
+        // byte array8 greater than 128 bytes
+        doEncodeDecodeByteArrayTestImpl(255, true);
+    }
+
+    @Test
+    public void testEncodeDecodeByteArray384FS() throws Throwable {
+        // byte array32
+        doEncodeDecodeByteArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeByteArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] source = createPayloadArrayBytes(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 254 ? 1 : 4; // less than 254 and not 256, since we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int arrayPayloadSize = encodingWidth + 1 + count; // variable width for element count + byte type descriptor + number of elements
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code + variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x51); // 'byte' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                expectedEncodingWrapper.writeByte(source[i]);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(byte.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (byte[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static byte[] createPayloadArrayBytes(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        byte[] payload = new byte[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = (byte) (64 + 1 + rand.nextInt(9));
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeShortArray50() throws Throwable {
+        // short array8 less than 128 bytes
+        doEncodeDecodeShortArrayTestImpl(50, false);
+    }
+
+    @Test
+    public void testEncodeDecodeShortArray100() throws Throwable {
+        // short array8 greater than 128 bytes
+        doEncodeDecodeShortArrayTestImpl(100, false);
+    }
+
+    @Test
+    public void testEncodeDecodeShortArray384() throws Throwable {
+        // short array32
+        doEncodeDecodeShortArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeShortArray50FS() throws Throwable {
+        // short array8 less than 128 bytes
+        doEncodeDecodeShortArrayTestImpl(50, true);
+    }
+
+    @Test
+    public void testEncodeDecodeShortArray100FS() throws Throwable {
+        // short array8 greater than 128 bytes
+        doEncodeDecodeShortArrayTestImpl(100, true);
+    }
+
+    @Test
+    public void testEncodeDecodeShortArray384FS() throws Throwable {
+        // short array32
+        doEncodeDecodeShortArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeShortArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        short[] source = createPayloadArrayShorts(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 127 ? 1 : 4; // less than 127, since each element is 2 bytes, but we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int arrayPayloadSize =  encodingWidth + 1 + (count * 2); // variable width for element count + byte type descriptor + (number of elements * size)
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x61); // 'short' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                expectedEncodingWrapper.writeShort(source[i]);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(short.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (short[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static short[] createPayloadArrayShorts(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        short[] payload = new short[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = (short) (64 + 1 + rand.nextInt(9));
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeIntArray10() throws Throwable {
+        // int array8 less than 128 bytes
+        doEncodeDecodeIntArrayTestImpl(10, false);
+    }
+
+    @Test
+    public void testEncodeDecodeIntArray50() throws Throwable {
+        // int array8 greater than 128 bytes
+        doEncodeDecodeIntArrayTestImpl(50, false);
+    }
+
+    @Test
+    public void testEncodeDecodeIntArray384() throws Throwable {
+        // int array32
+        doEncodeDecodeIntArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeIntArray10FS() throws Throwable {
+        // int array8 less than 128 bytes
+        doEncodeDecodeIntArrayTestImpl(10, true);
+    }
+
+    @Test
+    public void testEncodeDecodeIntArray50FS() throws Throwable {
+        // int array8 greater than 128 bytes
+        doEncodeDecodeIntArrayTestImpl(50, true);
+    }
+
+    @Test
+    public void testEncodeDecodeIntArray384FS() throws Throwable {
+        // int array32
+        doEncodeDecodeIntArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeIntArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        int[] source = createPayloadArrayInts(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 63 ? 1 : 4; // less than 63, since each element is 4 bytes, but we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int elementWidth = 4;
+            int arrayPayloadSize =  encodingWidth + 1 + (count * elementWidth); // variable width for element count + byte type descriptor + (number of elements * size)
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x71); // 'int' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                int j = source[i];
+                expectedEncodingWrapper.writeInt(j);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(int.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (int[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static int[] createPayloadArrayInts(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        int[] payload = new int[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = 128 + 1 + rand.nextInt(9);
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeLongArray10() throws Throwable {
+        // long array8 less than 128 bytes
+        doEncodeDecodeLongArrayTestImpl(10, false);
+    }
+
+    @Test
+    public void testEncodeDecodeLongArray25() throws Throwable {
+        // long array8 greater than 128 bytes
+        doEncodeDecodeLongArrayTestImpl(25, false);
+    }
+
+    @Test
+    public void testEncodeDecodeLongArray384() throws Throwable {
+        // long array32
+        doEncodeDecodeLongArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeLongArray10FS() throws Throwable {
+        // long array8 less than 128 bytes
+        doEncodeDecodeLongArrayTestImpl(10, false);
+    }
+
+    @Test
+    public void testEncodeDecodeLongArray25FS() throws Throwable {
+        // long array8 greater than 128 bytes
+        doEncodeDecodeLongArrayTestImpl(25, false);
+    }
+
+    @Test
+    public void testEncodeDecodeLongArray384FS() throws Throwable {
+        // long array32
+        doEncodeDecodeLongArrayTestImpl(384, false);
+    }
+
+    private void doEncodeDecodeLongArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        long[] source = createPayloadArrayLongs(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 31 ? 1 : 4; // less than 31, since each element is 8 bytes, but we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int elementWidth = 8;
+
+            int arrayPayloadSize = encodingWidth + 1 + (count * elementWidth); // variable width for element count + byte type descriptor + (number of elements * size)
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x81); // 'long' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                long j = source[i];
+                expectedEncodingWrapper.writeLong(j);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(long.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (long[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static long[] createPayloadArrayLongs(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        long[] payload = new long[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = 128 + 1 + rand.nextInt(9);
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeFloatArray25() throws Throwable {
+        // float array8 less than 128 bytes
+        doEncodeDecodeFloatArrayTestImpl(25, false);
+    }
+
+    @Test
+    public void testEncodeDecodeFloatArray50() throws Throwable {
+        // float array8 greater than 128 bytes
+        doEncodeDecodeFloatArrayTestImpl(50, false);
+    }
+
+    @Test
+    public void testEncodeDecodeFloatArray384() throws Throwable {
+        // float array32
+        doEncodeDecodeFloatArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeFloatArray25FS() throws Throwable {
+        // float array8 less than 128 bytes
+        doEncodeDecodeFloatArrayTestImpl(25, true);
+    }
+
+    @Test
+    public void testEncodeDecodeFloatArray50FS() throws Throwable {
+        // float array8 greater than 128 bytes
+        doEncodeDecodeFloatArrayTestImpl(50, true);
+    }
+
+    @Test
+    public void testEncodeDecodeFloatArray384FS() throws Throwable {
+        // float array32
+        doEncodeDecodeFloatArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeFloatArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        float[] source = createPayloadArrayFloats(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 63 ? 1 : 4; // less than 63, since each element is 4 bytes, but we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int arrayPayloadSize =  encodingWidth + 1 + (count * 4); // variable width for element count + byte type descriptor + (number of elements * size)
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x72); // 'float' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                expectedEncodingWrapper.writeFloat(source[i]);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(float.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (float[]) decoded, 0.0F, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static float[] createPayloadArrayFloats(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        float[] payload = new float[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = 64 + 1 + rand.nextInt(9);
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeDoubleArray10() throws Throwable {
+        // double array8 less than 128 bytes
+        doEncodeDecodeDoubleArrayTestImpl(10, false);
+    }
+
+    @Test
+    public void testEncodeDecodeDoubleArray25() throws Throwable {
+        // double array8 greater than 128 bytes
+        doEncodeDecodeDoubleArrayTestImpl(25, false);
+    }
+
+    @Test
+    public void testEncodeDecodeDoubleArray384() throws Throwable {
+        // double array32
+        doEncodeDecodeDoubleArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeDoubleArray10FS() throws Throwable {
+        // double array8 less than 128 bytes
+        doEncodeDecodeDoubleArrayTestImpl(10, true);
+    }
+
+    @Test
+    public void testEncodeDecodeDoubleArray25FS() throws Throwable {
+        // double array8 greater than 128 bytes
+        doEncodeDecodeDoubleArrayTestImpl(25, true);
+    }
+
+    @Test
+    public void testEncodeDecodeDoubleArray384FS() throws Throwable {
+        // double array32
+        doEncodeDecodeDoubleArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeDoubleArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        double[] source = createPayloadArrayDoubles(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 31 ? 1 : 4; // less than 31, since each element is 8 bytes, but we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int arrayPayloadSize =  encodingWidth + 1 + (count * 8); // variable width for element count + byte type descriptor + (number of elements * size)
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x82); // 'double' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                expectedEncodingWrapper.writeDouble(source[i]);
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(double.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (double[]) decoded, 0.0F, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static double[] createPayloadArrayDoubles(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        double[] payload = new double[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = 64 + 1 + rand.nextInt(9);
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testEncodeDecodeCharArray25() throws Throwable {
+        // char array8 less than 128 bytes
+        doEncodeDecodeCharArrayTestImpl(25, false);
+    }
+
+    @Test
+    public void testEncodeDecodeCharArray50() throws Throwable {
+        // char array8 greater than 128 bytes
+        doEncodeDecodeCharArrayTestImpl(50, false);
+    }
+
+    @Test
+    public void testEncodeDecodeCharArray384() throws Throwable {
+        // char array32
+        doEncodeDecodeCharArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeCharArray25FS() throws Throwable {
+        // char array8 less than 128 bytes
+        doEncodeDecodeCharArrayTestImpl(25, true);
+    }
+
+    @Test
+    public void testEncodeDecodeCharArray50FS() throws Throwable {
+        // char array8 greater than 128 bytes
+        doEncodeDecodeCharArrayTestImpl(50, true);
+    }
+
+    @Test
+    public void testEncodeDecodeCharArray384FS() throws Throwable {
+        // char array32
+        doEncodeDecodeCharArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeCharArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        char[] source = createPayloadArrayChars(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = count < 63 ? 1 : 4; // less than 63, since each element is 4 bytes, but we also need 1 byte for element count, and (in this case) 1 byte for primitive element type constructor.
+            int arrayPayloadSize =  encodingWidth + 1 + (count * 4); // variable width for element count + byte type descriptor + (number of elements * size)
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code +  variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            if (count < 254) {
+                expectedEncodingWrapper.writeByte((byte) 0xE0); // 'array8' type descriptor code
+                expectedEncodingWrapper.writeByte((byte) arrayPayloadSize);
+                expectedEncodingWrapper.writeByte((byte) count);
+            } else {
+                expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+                expectedEncodingWrapper.writeInt(arrayPayloadSize);
+                expectedEncodingWrapper.writeInt(count);
+            }
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0x73); // 'char' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                expectedEncodingWrapper.writeInt(source[i]); //4 byte encoding
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertTrue(decoded.getClass().getComponentType().isPrimitive());
+            assertEquals(char.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (char[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    private static char[] createPayloadArrayChars(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        char[] payload = new char[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = (char) (64 + 1 + rand.nextInt(9));
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testSkipValueSmallByteArray() throws IOException {
+        doTestSkipValueOnArrayOfSize(200, false);
+    }
+
+    @Test
+    public void testSkipValueLargeByteArray() throws IOException {
+        doTestSkipValueOnArrayOfSize(1024, false);
+    }
+
+    @Test
+    public void testSkipValueSmallByteArrayFromStream() throws IOException {
+        doTestSkipValueOnArrayOfSize(200, true);
+    }
+
+    @Test
+    public void testSkipValueLargeByteArrayFromStream() throws IOException {
+        doTestSkipValueOnArrayOfSize(1024, true);
+    }
+
+    private void doTestSkipValueOnArrayOfSize(int arraySize, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Random filler = new Random();
+        filler.setSeed(System.nanoTime());
+
+        byte[] bytes = new byte[arraySize];
+        filler.nextBytes(bytes);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeArray(buffer, encoderState, bytes);
+        }
+
+        byte[] expected = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        final Object result;
+
+        if (fromStream) {
+            for (int i = 0; i < 10; ++i) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Object.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder.isArrayType());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            }
+
+            result = decoder.readObject(buffer, decoderState);
+        } else {
+            for (int i = 0; i < 10; ++i) {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Object.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder.isArrayType());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof byte[]);
+
+        byte[] value = (byte[]) result;
+        assertArrayEquals(expected, value);
+    }
+
+    @Test
+    public void testArrayOfInts() throws IOException {
+        doTestArrayOfInts(false);
+    }
+
+    @Test
+    public void testArrayOfIntsFromStream() throws IOException {
+        doTestArrayOfInts(true);
+    }
+
+    public void doTestArrayOfInts(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        int[] source = new int[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = random.nextInt();
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        int[] array = (int[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/BinaryTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/BinaryTypeCodecTest.java
new file mode 100644
index 0000000..84cb19d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/BinaryTypeCodecTest.java
@@ -0,0 +1,524 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.BinaryTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/**
+ * Test the Binary codec for correctness
+ */
+public class BinaryTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFromStream() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readBinary(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readBinaryAsBuffer(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readBinary(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readBinaryAsBuffer(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        buffer.writeByte(EncodingCodes.NULL);
+        assertNull(decoder.readBinary(buffer, decoderState));
+    }
+
+    @Test
+    public void testReadFromNullEncodingCodeFromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        buffer.writeByte(EncodingCodes.NULL);
+        assertNull(streamDecoder.readBinary(stream, streamDecoderState));
+    }
+
+    @Test
+    public void testEncodeDecodeEmptyArrayBinary() throws Exception {
+        testEncodeDecodeEmptyArrayBinary(false);
+    }
+
+    @Test
+    public void testEncodeDecodeEmptyArrayBinaryFromStream() throws Exception {
+        testEncodeDecodeEmptyArrayBinary(true);
+    }
+
+    private void testEncodeDecodeEmptyArrayBinary(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Binary input = new Binary(new byte[0]);
+
+        encoder.writeBinary(buffer, encoderState, input);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof Binary);
+        Binary output = (Binary) result;
+
+        assertEquals(0, output.getLength());
+        assertEquals(0, output.getArrayOffset());
+        assertNotNull(output.getArray());
+    }
+
+    @Test
+    public void testEncodeDecodeBinary() throws Exception {
+        testEncodeDecodeBinary(false);
+    }
+
+    @Test
+    public void testEncodeDecodeBinaryFromStream() throws Exception {
+        testEncodeDecodeBinary(true);
+    }
+
+    private void testEncodeDecodeBinary(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Binary input = new Binary(new byte[] {0, 1, 2, 3, 4});
+
+        encoder.writeBinary(buffer, encoderState, input);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof Binary);
+        Binary output = (Binary) result;
+
+        assertEquals(5, output.getLength());
+        assertEquals(0, output.getArrayOffset());
+        assertNotNull(output.getArray());
+        assertEquals(input, output);
+        assertArrayEquals(input.getArray(), output.getArray());
+    }
+
+    @Test
+    public void testEncodeDecodeBinaryUsingRawBytesWithSmallArray() throws Exception {
+        testEncodeDecodeBinaryUsingRawBytesWithSmallArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeBinaryUsingRawBytesWithSmallArrayFromStream() throws Exception {
+        testEncodeDecodeBinaryUsingRawBytesWithSmallArray(true);
+    }
+
+    private void testEncodeDecodeBinaryUsingRawBytesWithSmallArray(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random filler = new Random();
+        filler.setSeed(System.nanoTime());
+
+        byte[] input = new byte[16];
+        filler.nextBytes(input);
+
+        encoder.writeBinary(buffer, encoderState, input);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof Binary);
+        Binary output = (Binary) result;
+
+        assertEquals(input.length, output.getLength());
+        assertEquals(0, output.getArrayOffset());
+        assertNotNull(output.getArray());
+        assertArrayEquals(input, output.getArray());
+    }
+
+    @Test
+    public void testEncodeDecodeBinaryUsingRawBytesWithLargeArray() throws Exception {
+        testEncodeDecodeBinaryUsingRawBytesWithLargeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeBinaryUsingRawBytesWithLargeArrayFromStream() throws Exception {
+        testEncodeDecodeBinaryUsingRawBytesWithLargeArray(true);
+    }
+
+    private void testEncodeDecodeBinaryUsingRawBytesWithLargeArray(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random filler = new Random();
+        filler.setSeed(System.nanoTime());
+
+        byte[] input = new byte[512];
+        filler.nextBytes(input);
+
+        encoder.writeBinary(buffer, encoderState, input);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof Binary);
+        Binary output = (Binary) result;
+
+        assertEquals(input.length, output.getLength());
+        assertEquals(0, output.getArrayOffset());
+        assertNotNull(output.getArray());
+        assertArrayEquals(input, output.getArray());
+    }
+
+    @Test
+    public void testDecodeFailsEarlyOnInvliadBinaryLengthVBin8() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.VBIN8);
+        buffer.writeByte(255);
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not be able to read binary with length greater than readable bytes");
+        } catch (IllegalArgumentException iae) {}
+
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testDecodeFailsEarlyOnInvliadBinaryLengthVBin32() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("Should not be able to read binary with length greater than readable bytes");
+        } catch (IllegalArgumentException iae) {}
+
+        assertEquals(5, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testDecodeAsBufferFailsEarlyOnInvliadBinaryLengthVBin32() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        try {
+            decoder.readBinaryAsBuffer(buffer, decoderState);
+            fail("Should not be able to read binary with length greater than readable bytes");
+        } catch (IllegalArgumentException iae) {}
+
+        assertEquals(5, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testDecodeOfBinaryTagFailsEarlyOnInvliadBinaryLengthVBin32() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(Integer.MAX_VALUE);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        try {
+            decoder.readDeliveryTag(buffer, decoderState);
+            fail("Should not be able to read binary with length greater than readable bytes");
+        } catch (DecodeException dex) {}
+    }
+
+    @Test
+    public void testSkipFailsEarlyOnInvliadBinaryLengthVBin8() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.VBIN8);
+        buffer.writeByte(255);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+        assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.VBIN8 & 0xFF);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.skipValue(buffer, decoderState);
+            fail("Should not be able to skip binary with length greater than readable bytes");
+        } catch (IllegalArgumentException ex) {}
+
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testSkipFailsEarlyOnInvliadBinaryLengthVBin8FromStream() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.VBIN8);
+        buffer.writeByte(255);
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+        assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.VBIN8 & 0xFF);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+        } catch (IllegalArgumentException ex) {
+            fail("Should be able to skip binary with length greater than readable bytes");
+        }
+
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testSkipFailsEarlyOnInvliadBinaryLengthVBin32() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+        assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.VBIN32 & 0xFF);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.skipValue(buffer, decoderState);
+            fail("Should not be able to skip binary with length greater than readable bytes");
+        } catch (IllegalArgumentException ex) {}
+
+        assertEquals(5, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testSkipFailsEarlyOnInvliadBinaryLengthVBin32FromStream() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.VBIN32);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+        assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.VBIN32 & 0xFF);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+        } catch (IllegalArgumentException ex) {
+            fail("Should be able to skip binary with length greater than readable bytes");
+        }
+
+        assertEquals(5, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testReadEncodedSizeFromVBin8Encoding() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.VBIN8);
+        buffer.writeByte(255);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+        BinaryTypeDecoder binaryDecoder = (BinaryTypeDecoder) typeDecoder;
+        assertEquals(255, binaryDecoder.readSize(buffer));
+
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testReadEncodedSizeFromVBin8EncodingUsingStream() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.VBIN8);
+        buffer.writeByte(255);
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+        BinaryTypeDecoder binaryDecoder = (BinaryTypeDecoder) typeDecoder;
+        assertEquals(255, binaryDecoder.readSize(stream));
+
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testZeroSizedArrayOfBinaryObjects() throws IOException {
+        testZeroSizedArrayOfBinaryObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfBinaryObjectsFromStream() throws IOException {
+        testZeroSizedArrayOfBinaryObjects(true);
+    }
+
+    private void testZeroSizedArrayOfBinaryObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Binary[] source = new Binary[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Binary[] array = (Binary[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testArrayOfBinaryObjects() throws IOException {
+        testArrayOfBinaryObjects(false);
+    }
+
+    @Test
+    public void testArrayOfBinaryObjectsFromStream() throws IOException {
+        testArrayOfBinaryObjects(true);
+    }
+
+    private void testArrayOfBinaryObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random filler = new Random();
+        filler.setSeed(System.nanoTime());
+
+        Binary[] source = new Binary[5];
+        for (int i = 0; i < source.length; ++i) {
+            byte[] data = new byte[16 * i];
+            filler.nextBytes(data);
+
+            source[i] = new Binary(data);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Binary[] array = (Binary[]) result;
+        assertEquals(source.length, array.length);
+
+        for (int i = 0; i < source.length; ++i) {
+            Binary decoded = ((Binary[]) result)[i];
+            assertArrayEquals(source[i].getArray(), decoded.getArray());
+        }
+    }
+
+    @Test
+    public void testStreamSkipOfEncodingHandlesIOException() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Random filler = new Random();
+        filler.setSeed(System.nanoTime());
+
+        byte[] input = new byte[512];
+        filler.nextBytes(input);
+
+        encoder.writeBinary(buffer, encoderState, input);
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertEquals(Binary.class, typeDecoder.getTypeClass());
+
+        stream = Mockito.spy(stream);
+
+        Mockito.when(stream.skip(Mockito.anyLong())).thenThrow(EOFException.class);
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+            fail("Expected an exception on skip of encoded list failure.");
+        } catch (DecodeException dex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/BooleanTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/BooleanTypeCodecTest.java
new file mode 100644
index 0000000..cbd4daa
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/BooleanTypeCodecTest.java
@@ -0,0 +1,963 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveArrayTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test the BooleanTypeDecoder for correctness
+ */
+public class BooleanTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsBoolean() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsBoolean(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsBooleanFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsBoolean(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsBoolean(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readBoolean(stream, streamDecoderState);
+                fail("Should not allow read of integer type as boolean");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readBoolean(stream, streamDecoderState, false);
+                fail("Should not allow read of integer type as boolean");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readBoolean(buffer, decoderState);
+                fail("Should not allow read of integer type as boolean");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readBoolean(buffer, decoderState, false);
+                fail("Should not allow read of integer type as boolean");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanEncodedBytes() throws Exception {
+        testDecodeBooleanEncodedBytes(false);
+    }
+
+    @Test
+    public void testDecodeBooleanEncodedBytesFS() throws Exception {
+        testDecodeBooleanEncodedBytes(true);
+    }
+
+    private void testDecodeBooleanEncodedBytes(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.BOOLEAN_TRUE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.BOOLEAN_FALSE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(1);
+
+        if (fromStream) {
+            assertNull(streamDecoder.readBoolean(stream, streamDecoderState));
+
+            boolean result1 = streamDecoder.readBoolean(stream, streamDecoderState);
+            boolean result2 = streamDecoder.readBoolean(stream, streamDecoderState);
+            boolean result3 = streamDecoder.readBoolean(stream, streamDecoderState);
+            boolean result4 = streamDecoder.readBoolean(stream, streamDecoderState);
+
+            assertTrue(result1);
+            assertFalse(result2);
+            assertFalse(result3);
+            assertTrue(result4);
+        } else {
+            assertNull(decoder.readBoolean(buffer, decoderState));
+
+            boolean result1 = decoder.readBoolean(buffer, decoderState);
+            boolean result2 = decoder.readBoolean(buffer, decoderState);
+            boolean result3 = decoder.readBoolean(buffer, decoderState);
+            boolean result4 = decoder.readBoolean(buffer, decoderState);
+
+            assertTrue(result1);
+            assertFalse(result2);
+            assertFalse(result3);
+            assertTrue(result4);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanEncodedBytesWithTypeDecoder() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(false);
+    }
+
+    @Test
+    public void testDecodeBooleanEncodedBytesWithTypeDecoderFS() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(true);
+    }
+
+    private void testDecodeBooleanEncodedBytesWithTypeDecoder(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.BOOLEAN_TRUE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.BOOLEAN_FALSE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(1);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((boolean) typeDecoder.readValue(stream, streamDecoderState));
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((boolean) typeDecoder.readValue(stream, streamDecoderState));
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((boolean) typeDecoder.readValue(stream, streamDecoderState));
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((boolean) typeDecoder.readValue(stream, streamDecoderState));
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((boolean) typeDecoder.readValue(buffer, decoderState));
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((boolean) typeDecoder.readValue(buffer, decoderState));
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((boolean) typeDecoder.readValue(buffer, decoderState));
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((boolean) typeDecoder.readValue(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanTrueArray32() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY32, EncodingCodes.BOOLEAN_TRUE, false);
+    }
+
+    @Test
+    public void testDecodeBooleanTrueArray32FromStream() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY32, EncodingCodes.BOOLEAN_TRUE, true);
+    }
+
+    @Test
+    public void testDecodeBooleanFalseArray32() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY32, EncodingCodes.BOOLEAN_FALSE, false);
+    }
+
+    @Test
+    public void testDecodeBooleanFalseArray32FromStream() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY32, EncodingCodes.BOOLEAN_FALSE, true);
+    }
+
+    @Test
+    public void testDecodeBooleanTrueArray8() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY8, EncodingCodes.BOOLEAN_TRUE, false);
+    }
+
+    @Test
+    public void testDecodeBooleanTrueArray8FromStream() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY8, EncodingCodes.BOOLEAN_TRUE, true);
+    }
+
+    @Test
+    public void testDecodeBooleanFalseArray8() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY8, EncodingCodes.BOOLEAN_FALSE, false);
+    }
+
+    @Test
+    public void testDecodeBooleanFalseArray8FromStream() throws Exception {
+        testDecodeBooleanEncodedBytesWithTypeDecoder(EncodingCodes.ARRAY8, EncodingCodes.BOOLEAN_FALSE, true);
+    }
+
+    private void testDecodeBooleanEncodedBytesWithTypeDecoder(byte arrayType, byte encodingCode, boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        if (arrayType == EncodingCodes.ARRAY32) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(3);  // Size
+            buffer.writeInt(10); // Count
+            buffer.writeByte(encodingCode);
+        } else {
+            buffer.writeByte(EncodingCodes.ARRAY8);
+            buffer.writeByte(3);  // Size
+            buffer.writeByte(10); // Count
+            buffer.writeByte(encodingCode);
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertTrue(typeDecoder instanceof PrimitiveArrayTypeDecoder);
+            PrimitiveArrayTypeDecoder arrayDecoder = (PrimitiveArrayTypeDecoder) typeDecoder;
+            boolean[] booleans = (boolean[]) arrayDecoder.readValue(stream, streamDecoderState);
+            assertEquals(10, booleans.length);
+            for (boolean value : booleans) {
+                assertEquals(encodingCode == EncodingCodes.BOOLEAN_TRUE, value);
+            }
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertTrue(typeDecoder instanceof PrimitiveArrayTypeDecoder);
+            PrimitiveArrayTypeDecoder arrayDecoder = (PrimitiveArrayTypeDecoder) typeDecoder;
+            boolean[] booleans = (boolean[]) arrayDecoder.readValue(buffer, decoderState);
+            assertEquals(10, booleans.length);
+            for (boolean value : booleans) {
+                assertEquals(encodingCode == EncodingCodes.BOOLEAN_TRUE, value);
+            }
+        }
+    }
+
+    @Test
+    public void testPeekNextTypeDecoder() throws Exception {
+        testPeekNextTypeDecoder(false);
+    }
+
+    @Test
+    public void testPeekNextTypeDecoderFS() throws Exception {
+        testPeekNextTypeDecoder(true);
+    }
+
+    private void testPeekNextTypeDecoder(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.BOOLEAN_TRUE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.BOOLEAN_FALSE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(1);
+
+        if (fromStream) {
+            assertEquals(Boolean.class, streamDecoder.peekNextTypeDecoder(stream, streamDecoderState).getTypeClass());
+            assertTrue(streamDecoder.readBoolean(stream, streamDecoderState));
+            assertEquals(Boolean.class, streamDecoder.peekNextTypeDecoder(stream, streamDecoderState).getTypeClass());
+            assertFalse(streamDecoder.readBoolean(stream, streamDecoderState));
+            assertEquals(Boolean.class, streamDecoder.peekNextTypeDecoder(stream, streamDecoderState).getTypeClass());
+            assertFalse(streamDecoder.readBoolean(stream, streamDecoderState));
+            assertEquals(Boolean.class, streamDecoder.peekNextTypeDecoder(stream, streamDecoderState).getTypeClass());
+            assertTrue(streamDecoder.readBoolean(stream, streamDecoderState));
+        } else {
+            assertEquals(Boolean.class, decoder.peekNextTypeDecoder(buffer, decoderState).getTypeClass());
+            assertTrue(decoder.readBoolean(buffer, decoderState));
+            assertEquals(Boolean.class, decoder.peekNextTypeDecoder(buffer, decoderState).getTypeClass());
+            assertFalse(decoder.readBoolean(buffer, decoderState));
+            assertEquals(Boolean.class, decoder.peekNextTypeDecoder(buffer, decoderState).getTypeClass());
+            assertFalse(decoder.readBoolean(buffer, decoderState));
+            assertEquals(Boolean.class, decoder.peekNextTypeDecoder(buffer, decoderState).getTypeClass());
+            assertTrue(decoder.readBoolean(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanEncodedBytesAsPrimtives() throws Exception {
+        testDecodeBooleanEncodedBytesAsPrimtives(false);
+    }
+
+    @Test
+    public void testDecodeBooleanEncodedBytesAsPrimtivesFS() throws Exception {
+        testDecodeBooleanEncodedBytesAsPrimtives(true);
+    }
+
+    private void testDecodeBooleanEncodedBytesAsPrimtives(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.BOOLEAN_TRUE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.BOOLEAN_FALSE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(1);
+
+        if (fromStream) {
+            boolean result1 = streamDecoder.readBoolean(stream, streamDecoderState, false);
+            boolean result2 = streamDecoder.readBoolean(stream, streamDecoderState, true);
+            boolean result3 = streamDecoder.readBoolean(stream, streamDecoderState, true);
+            boolean result4 = streamDecoder.readBoolean(stream, streamDecoderState, false);
+
+            assertTrue(result1);
+            assertFalse(result2);
+            assertFalse(result3);
+            assertTrue(result4);
+        } else {
+            boolean result1 = decoder.readBoolean(buffer, decoderState, false);
+            boolean result2 = decoder.readBoolean(buffer, decoderState, true);
+            boolean result3 = decoder.readBoolean(buffer, decoderState, true);
+            boolean result4 = decoder.readBoolean(buffer, decoderState, false);
+
+            assertTrue(result1);
+            assertFalse(result2);
+            assertFalse(result3);
+            assertTrue(result4);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanTrue() throws Exception {
+        testDecodeBooleanTrue(false);
+    }
+
+    @Test
+    public void testDecodeBooleanTrueFS() throws Exception {
+        testDecodeBooleanTrue(true);
+    }
+
+    private void testDecodeBooleanTrue(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeBoolean(buffer, encoderState, true);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+        assertTrue(result instanceof Boolean);
+        assertTrue(((Boolean) result).booleanValue());
+
+        encoder.writeBoolean(buffer, encoderState, true);
+
+        final Boolean booleanResult;
+        if (fromStream) {
+            booleanResult = streamDecoder.readBoolean(stream, streamDecoderState);
+        } else {
+            booleanResult = decoder.readBoolean(buffer, decoderState);
+        }
+        assertTrue(booleanResult.booleanValue());
+        assertEquals(Boolean.TRUE, booleanResult);
+    }
+
+    @Test
+    public void testDecodeBooleanFalse() throws Exception {
+        testDecodeBooleanFalse(false);
+    }
+
+    @Test
+    public void testDecodeBooleanFalseFS() throws Exception {
+        testDecodeBooleanFalse(true);
+    }
+
+    private void testDecodeBooleanFalse(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeBoolean(buffer, encoderState, false);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof Boolean);
+        assertFalse(((Boolean) result).booleanValue());
+    }
+
+    @Test
+    public void testDecodeBooleanFromNullEncoding() throws Exception {
+        testDecodeBooleanFromNullEncoding(false);
+    }
+
+    @Test
+    public void testDecodeBooleanFromNullEncodingFS() throws Exception {
+        testDecodeBooleanFromNullEncoding(true);
+    }
+
+    private void testDecodeBooleanFromNullEncoding(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeBoolean(buffer, encoderState, true);
+        encoder.writeNull(buffer, encoderState);
+
+        final boolean result;
+        if (fromStream) {
+            result = streamDecoder.readBoolean(stream, streamDecoderState);
+        } else {
+            result = decoder.readBoolean(buffer, decoderState);
+        }
+        assertTrue(result);
+        assertNull(decoder.readBoolean(buffer, decoderState));
+    }
+
+    @Test
+    public void testDecodeBooleanAsPrimitiveWithDefault() throws Exception {
+        testDecodeBooleanAsPrimitiveWithDefault(false);
+    }
+
+    @Test
+    public void testDecodeBooleanAsPrimitiveWithDefaultFS() throws Exception {
+        testDecodeBooleanAsPrimitiveWithDefault(true);
+    }
+
+    private void testDecodeBooleanAsPrimitiveWithDefault(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeBoolean(buffer, encoderState, true);
+        encoder.writeNull(buffer, encoderState);
+
+        if (fromStream) {
+            boolean result = streamDecoder.readBoolean(stream, streamDecoderState, false);
+            assertTrue(result);
+            result = streamDecoder.readBoolean(stream, streamDecoderState, false);
+            assertFalse(result);
+        } else {
+            boolean result = decoder.readBoolean(buffer, decoderState, false);
+            assertTrue(result);
+            result = decoder.readBoolean(buffer, decoderState, false);
+            assertFalse(result);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanFailsForNonBooleanType() throws Exception {
+        testDecodeBooleanFailsForNonBooleanType(false);
+    }
+
+    @Test
+    public void testDecodeBooleanFailsForNonBooleanTypeFS() throws Exception {
+        testDecodeBooleanFailsForNonBooleanType(true);
+    }
+
+    private void testDecodeBooleanFailsForNonBooleanType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeLong(buffer, encoderState, 1l);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readBoolean(stream, streamDecoderState);
+                fail("Should not read long as boolean value.");
+            } catch (DecodeException ioex) {}
+        } else {
+            try {
+                decoder.readBoolean(buffer, decoderState);
+                fail("Should not read long as boolean value.");
+            } catch (DecodeException ioex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfBooleans() throws IOException {
+        doTestDecodeBooleanSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfBooleans() throws IOException {
+        doTestDecodeBooleanSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfBooleansFS() throws IOException {
+        doTestDecodeBooleanSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfBooleansFS() throws IOException {
+        doTestDecodeBooleanSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeBooleanSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeBoolean(buffer, encoderState, i % 2 == 0);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Boolean);
+
+            Boolean boolValue = (Boolean) result;
+            assertEquals(i % 2 == 0, boolValue.booleanValue());
+        }
+    }
+
+    @Test
+    public void testArrayOfBooleanObjects() throws IOException {
+        testArrayOfBooleanObjects(false);
+    }
+
+    @Test
+    public void testArrayOfBooleanObjectsFS() throws IOException {
+        testArrayOfBooleanObjects(true);
+    }
+
+    private void testArrayOfBooleanObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Boolean[] source = new Boolean[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = i % 2 == 0;
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        boolean[] array = (boolean[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfBooleanObjects() throws IOException {
+        testZeroSizedArrayOfBooleanObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfBooleanObjectsFS() throws IOException {
+        testZeroSizedArrayOfBooleanObjects(true);
+    }
+
+    private void testZeroSizedArrayOfBooleanObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Boolean[] source = new Boolean[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        boolean[] array = (boolean[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testDecodeSmallBooleanArray() throws IOException {
+        doTestDecodeBooleanArrayType(SMALL_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeBooleanArray() throws IOException {
+        doTestDecodeBooleanArrayType(LARGE_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallBooleanArrayFS() throws IOException {
+        doTestDecodeBooleanArrayType(SMALL_ARRAY_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeBooleanArrayFS() throws IOException {
+        doTestDecodeBooleanArrayType(LARGE_ARRAY_SIZE, true);
+    }
+
+    private void doTestDecodeBooleanArrayType(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        boolean[] source = new boolean[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = i % 2 == 0;
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        boolean[] array = (boolean[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testArrayOfPrimitiveBooleanObjects() throws IOException {
+        testArrayOfPrimitiveBooleanObjects(false);
+    }
+
+    @Test
+    public void testArrayOfPrimitiveBooleanObjectsFS() throws IOException {
+        testArrayOfPrimitiveBooleanObjects(true);
+    }
+
+    private void testArrayOfPrimitiveBooleanObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        boolean[] source = new boolean[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = i % 2 == 0;
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        boolean[] array = (boolean[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfPrimitiveBooleanObjects() throws IOException {
+        testZeroSizedArrayOfPrimitiveBooleanObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfPrimitiveBooleanObjectsFS() throws IOException {
+        testZeroSizedArrayOfPrimitiveBooleanObjects(true);
+    }
+
+    private void testZeroSizedArrayOfPrimitiveBooleanObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        boolean[] source = new boolean[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        boolean[] array = (boolean[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testArrayOfArraysOfPrimitiveBooleanObjects() throws IOException {
+        testArrayOfArraysOfPrimitiveBooleanObjects(false);
+    }
+
+    @Test
+    public void testArrayOfArraysOfPrimitiveBooleanObjectsFS() throws IOException {
+        testArrayOfArraysOfPrimitiveBooleanObjects(true);
+    }
+
+    private void testArrayOfArraysOfPrimitiveBooleanObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        boolean[][] source = new boolean[2][size];
+        for (int i = 0; i < size; ++i) {
+            source[0][i] = i % 2 == 0;
+            source[1][i] = i % 2 == 0;
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Object[] resultArray = (Object[]) result;
+
+        assertNotNull(resultArray);
+        assertEquals(2, resultArray.length);
+
+        assertTrue(resultArray[0].getClass().isArray());
+        assertTrue(resultArray[1].getClass().isArray());
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            boolean[] nested = (boolean[]) resultArray[i];
+            assertEquals(source[i].length, nested.length);
+            assertArrayEquals(source[i], nested);
+        }
+    }
+
+    @Test
+    public void testReadAllBooleanTypeEncodings() throws IOException {
+        testReadAllBooleanTypeEncodings(false);
+    }
+
+    @Test
+    public void testReadAllBooleanTypeEncodingsFS() throws IOException {
+        testReadAllBooleanTypeEncodings(true);
+    }
+
+    private void testReadAllBooleanTypeEncodings(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.BOOLEAN_TRUE);
+        buffer.writeByte(EncodingCodes.BOOLEAN_FALSE);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(1);
+        buffer.writeByte(EncodingCodes.BOOLEAN);
+        buffer.writeByte(0);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((Boolean) typeDecoder.readValue(stream, streamDecoderState));
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((Boolean) typeDecoder.readValue(stream, streamDecoderState));
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((Boolean) typeDecoder.readValue(stream, streamDecoderState));
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((Boolean) typeDecoder.readValue(stream, streamDecoderState));
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((Boolean) typeDecoder.readValue(buffer, decoderState));
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((Boolean) typeDecoder.readValue(buffer, decoderState));
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertTrue((Boolean) typeDecoder.readValue(buffer, decoderState));
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Boolean.class, typeDecoder.getTypeClass());
+            assertFalse((Boolean) typeDecoder.readValue(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testSkipValueFullBooleanTypeEncodings() throws IOException {
+        testSkipValueFullBooleanTypeEncodings(false);
+    }
+
+    @Test
+    public void testSkipValueFullBooleanTypeEncodingsFS() throws IOException {
+        testSkipValueFullBooleanTypeEncodings(true);
+    }
+
+    private void testSkipValueFullBooleanTypeEncodings(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            buffer.writeByte(EncodingCodes.BOOLEAN);
+            buffer.writeByte(1);
+            buffer.writeByte(EncodingCodes.BOOLEAN);
+            buffer.writeByte(0);
+        }
+
+        encoder.writeObject(buffer, encoderState, false);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Boolean);
+
+        Boolean value = (Boolean) result;
+        assertEquals(false, value);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeBoolean(buffer, encoderState, Boolean.TRUE);
+            encoder.writeBoolean(buffer, encoderState, false);
+        }
+
+        encoder.writeObject(buffer, encoderState, false);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN_TRUE & 0xFF);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN_FALSE & 0xFF);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN_TRUE & 0xFF);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.BOOLEAN_FALSE & 0xFF);
+                assertEquals(Boolean.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Boolean);
+
+        Boolean value = (Boolean) result;
+        assertEquals(false, value);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ByteTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ByteTypeCodecTest.java
new file mode 100644
index 0000000..788d684
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ByteTypeCodecTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ByteTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.ByteTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class ByteTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readByte(stream, streamDecoderState);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readByte(stream, streamDecoderState, (byte) 0);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readByte(buffer, decoderState);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readByte(buffer, decoderState, (byte) 0);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.BYTE, (byte) new ByteTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Byte.class, new ByteTypeEncoder().getTypeClass());
+        assertEquals(Byte.class, new ByteTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testPeekNextTypeDecoder() throws IOException {
+        testPeekNextTypeDecoder(false);
+    }
+
+    @Test
+    public void testPeekNextTypeDecoderFS() throws IOException {
+        testPeekNextTypeDecoder(true);
+    }
+
+    private void testPeekNextTypeDecoder(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte((byte) 42);
+
+        if (fromStream) {
+            assertEquals(Byte.class, streamDecoder.peekNextTypeDecoder(stream, streamDecoderState).getTypeClass());
+            assertEquals(42, streamDecoder.readByte(stream, streamDecoderState).intValue());
+        } else {
+            assertEquals(Byte.class, decoder.peekNextTypeDecoder(buffer, decoderState).getTypeClass());
+            assertEquals(42, decoder.readByte(buffer, decoderState).intValue());
+        }
+    }
+
+    @Test
+    public void testReadByteFromEncodingCode() throws IOException {
+        testReadByteFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadByteFromEncodingCodeFS() throws IOException {
+        testReadByteFromEncodingCode(true);
+    }
+
+    private void testReadByteFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte((byte) 42);
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte((byte) 43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readByte(stream, streamDecoderState).intValue());
+            assertEquals(43, streamDecoder.readByte(stream, streamDecoderState, (byte) 42));
+            assertNull(streamDecoder.readByte(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readByte(stream, streamDecoderState, (byte) 42));
+        } else {
+            assertEquals(42, decoder.readByte(buffer, decoderState).intValue());
+            assertEquals(43, decoder.readByte(buffer, decoderState, (byte) 42));
+            assertNull(decoder.readByte(buffer, decoderState));
+            assertEquals(42, decoder.readByte(buffer, decoderState, (byte) 42));
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeByte(buffer, encoderState, Byte.MAX_VALUE);
+            encoder.writeByte(buffer, encoderState, (byte) 16);
+        }
+
+        byte expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Byte.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Byte.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Byte.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Byte.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Byte);
+
+        Byte value = (Byte) result;
+        assertEquals(expected, value.byteValue());
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int size = 10;
+
+        Byte[] source = new Byte[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Byte.valueOf((byte) (random.nextInt() & 0xFF));
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        byte[] array = (byte[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Byte[] source = new Byte[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        byte[] array = (byte[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/CharacterTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/CharacterTypeCodecTest.java
new file mode 100644
index 0000000..dd7bfd3
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/CharacterTypeCodecTest.java
@@ -0,0 +1,330 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.CharacterTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.CharacterTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class CharacterTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readCharacter(stream, streamDecoderState);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readCharacter(stream, streamDecoderState, (char) 0);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readCharacter(buffer, decoderState);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readCharacter(buffer, decoderState, (char) 0);
+                fail("Should not allow read of integer type as byte");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.CHAR);
+        buffer.writeInt(42);
+        buffer.writeByte(EncodingCodes.CHAR);
+        buffer.writeInt(43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readCharacter(stream, streamDecoderState).charValue());
+            assertEquals(43, streamDecoder.readCharacter(stream, streamDecoderState, (char) 42));
+            assertNull(streamDecoder.readCharacter(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readCharacter(stream, streamDecoderState, (char) 42));
+        } else {
+            assertEquals(42, decoder.readCharacter(buffer, decoderState).charValue());
+            assertEquals(43, decoder.readCharacter(buffer, decoderState, (char) 42));
+            assertNull(decoder.readCharacter(buffer, decoderState));
+            assertEquals(42, decoder.readCharacter(buffer, decoderState, (char) 42));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.CHAR, (byte) new CharacterTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Character.class, new CharacterTypeEncoder().getTypeClass());
+        assertEquals(Character.class, new CharacterTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadCharFromEncodingCode() throws IOException {
+        testReadCharFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadCharFromEncodingCodeFS() throws IOException {
+        testReadCharFromEncodingCode(true);
+    }
+
+    private void testReadCharFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.CHAR);
+        buffer.writeInt(42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readCharacter(stream, streamDecoderState).charValue());
+        } else {
+            assertEquals(42, decoder.readCharacter(buffer, decoderState).charValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeCharacter(buffer, encoderState, Character.MAX_VALUE);
+            encoder.writeCharacter(buffer, encoderState, (char) 16);
+        }
+
+        char expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Character.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Character.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Character.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Character.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Character);
+
+        Character value = (Character) result;
+        assertEquals(expected, value.charValue());
+    }
+
+    @Test
+    public void testArrayOfCharacterObjects() throws IOException {
+        testArrayOfCharacterObjects(false);
+    }
+
+    @Test
+    public void testArrayOfCharacterObjectsFS() throws IOException {
+        testArrayOfCharacterObjects(true);
+    }
+
+    private void testArrayOfCharacterObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Character[] source = new Character[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Character.valueOf((char) i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        char[] array = (char[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfCharacterObjects() throws IOException {
+        testZeroSizedArrayOfCharacterObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfCharacterObjectsFS() throws IOException {
+        testZeroSizedArrayOfCharacterObjects(true);
+    }
+
+    private void testZeroSizedArrayOfCharacterObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Character[] source = new Character[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        char[] array = (char[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testDecodeSmallCharArray() throws IOException {
+        doTestDecodeCharArrayType(SMALL_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeCharArray() throws IOException {
+        doTestDecodeCharArrayType(LARGE_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallCharArrayFS() throws IOException {
+        doTestDecodeCharArrayType(SMALL_ARRAY_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeCharArrayFS() throws IOException {
+        doTestDecodeCharArrayType(LARGE_ARRAY_SIZE, true);
+    }
+
+    private void doTestDecodeCharArrayType(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        char[] source = new char[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Character.valueOf((char) i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        char[] array = (char[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal128TypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal128TypeCodecTest.java
new file mode 100644
index 0000000..457469b
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal128TypeCodecTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal128TypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.Decimal128TypeEncoder;
+import org.apache.qpid.protonj2.types.Decimal128;
+import org.junit.jupiter.api.Test;
+
+public class Decimal128TypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readDecimal128(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readDecimal128(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DECIMAL128);
+        buffer.writeLong(42);
+        buffer.writeLong(43);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        final Decimal128 result;
+        if (fromStream) {
+            result = streamDecoder.readDecimal128(stream, streamDecoderState);
+        } else {
+            result = decoder.readDecimal128(buffer, decoderState);
+        }
+
+        assertEquals(42, result.getMostSignificantBits());
+        assertEquals(43, result.getLeastSignificantBits());
+
+        if (fromStream) {
+            assertNull(streamDecoder.readDecimal128(stream, streamDecoderState));
+        } else {
+            assertNull(decoder.readDecimal128(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.DECIMAL128, (byte) new Decimal128TypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Decimal128.class, new Decimal128TypeEncoder().getTypeClass());
+        assertEquals(Decimal128.class, new Decimal128TypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadFromEncodingCode() throws IOException {
+        testReadFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromEncodingCodeFS() throws IOException {
+        testReadFromEncodingCode(true);
+    }
+
+    private void testReadFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DECIMAL128);
+        buffer.writeLong(42);
+        buffer.writeLong(84);
+
+        final Decimal128 result;
+        if (fromStream) {
+            result = streamDecoder.readDecimal128(stream, streamDecoderState);
+        } else {
+            result = decoder.readDecimal128(buffer, decoderState);
+        }
+
+        assertEquals(42, result.getMostSignificantBits());
+        assertEquals(84, result.getLeastSignificantBits());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeDecimal128(buffer, encoderState, new Decimal128(Long.MAX_VALUE - i, 42));
+            encoder.writeDecimal128(buffer, encoderState, new Decimal128(i, i));
+        }
+
+        Decimal128 expected = new Decimal128(42, 42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Decimal128.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Decimal128.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Decimal128.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Decimal128.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Decimal128);
+
+        Decimal128 value = (Decimal128) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int size = 10;
+
+        Decimal128[] source = new Decimal128[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = new Decimal128(random.nextLong(), random.nextLong());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Decimal128[] array = (Decimal128[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Decimal128[] source = new Decimal128[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Decimal128[] array = (Decimal128[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal32TypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal32TypeCodecTest.java
new file mode 100644
index 0000000..c2c2e75
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal32TypeCodecTest.java
@@ -0,0 +1,264 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal32TypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.Decimal32TypeEncoder;
+import org.apache.qpid.protonj2.types.Decimal32;
+import org.junit.jupiter.api.Test;
+
+public class Decimal32TypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readDecimal32(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readDecimal32(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DECIMAL32);
+        buffer.writeInt(42);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readDecimal32(stream, streamDecoderState).getBits());
+            assertNull(streamDecoder.readDecimal32(stream, streamDecoderState));
+        } else {
+            assertEquals(42, decoder.readDecimal32(buffer, decoderState).getBits());
+            assertNull(decoder.readDecimal32(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.DECIMAL32, (byte) new Decimal32TypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Decimal32.class, new Decimal32TypeEncoder().getTypeClass());
+        assertEquals(Decimal32.class, new Decimal32TypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadFromEncodingCode() throws IOException {
+        testReadFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromEncodingCodeFS() throws IOException {
+        testReadFromEncodingCode(true);
+    }
+
+    private void testReadFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DECIMAL32);
+        buffer.writeInt(42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readDecimal32(stream, streamDecoderState).getBits());
+        } else {
+            assertEquals(42, decoder.readDecimal32(buffer, decoderState).getBits());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeDecimal32(buffer, encoderState, new Decimal32(Integer.MAX_VALUE - i));
+            encoder.writeDecimal32(buffer, encoderState, new Decimal32(i));
+        }
+
+        Decimal32 expected = new Decimal32(42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Decimal32.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Decimal32.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Decimal32.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Decimal32.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Decimal32);
+
+        Decimal32 value = (Decimal32) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Decimal32[] source = new Decimal32[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = new Decimal32(random.nextInt());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Decimal32[] array = (Decimal32[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Decimal32[] source = new Decimal32[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Decimal32[] array = (Decimal32[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal64TypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal64TypeCodecTest.java
new file mode 100644
index 0000000..a3821ea
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/Decimal64TypeCodecTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Decimal64TypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.Decimal64TypeEncoder;
+import org.apache.qpid.protonj2.types.Decimal64;
+import org.junit.jupiter.api.Test;
+
+public class Decimal64TypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readDecimal64(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readDecimal64(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DECIMAL64);
+        buffer.writeLong(42);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readDecimal64(stream, streamDecoderState).getBits());
+            assertNull(streamDecoder.readDecimal64(stream, streamDecoderState));
+        } else {
+            assertEquals(42, decoder.readDecimal64(buffer, decoderState).getBits());
+            assertNull(decoder.readDecimal64(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.DECIMAL64, (byte) new Decimal64TypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Decimal64.class, new Decimal64TypeEncoder().getTypeClass());
+        assertEquals(Decimal64.class, new Decimal64TypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadFromEncodingCode() throws IOException {
+        testReadFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromEncodingCodeFS() throws IOException {
+        testReadFromEncodingCode(true);
+    }
+
+    private void testReadFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DECIMAL64);
+        buffer.writeLong(42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readDecimal64(stream, streamDecoderState).getBits());
+        } else {
+            assertEquals(42, decoder.readDecimal64(buffer, decoderState).getBits());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeDecimal64(buffer, encoderState, new Decimal64(Long.MAX_VALUE - i));
+            encoder.writeDecimal64(buffer, encoderState, new Decimal64(i));
+        }
+
+        Decimal64 expected = new Decimal64(42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Decimal64.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Decimal64.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Decimal64.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Decimal64.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Decimal64);
+
+        Decimal64 value = (Decimal64) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int size = 10;
+
+        Decimal64[] source = new Decimal64[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = new Decimal64(random.nextLong());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Decimal64[] array = (Decimal64[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Decimal64[] source = new Decimal64[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        Decimal64[] array = (Decimal64[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/DoubleTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/DoubleTypeCodecTest.java
new file mode 100644
index 0000000..61880ac
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/DoubleTypeCodecTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.DoubleTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.DoubleTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class DoubleTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readDouble(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readDouble(stream, streamDecoderState, 0.0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readDouble(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readDouble(buffer, decoderState, 0.0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadPrimitiveTypeFromEncodingCode() throws IOException {
+        testReadPrimitiveTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadPrimitiveTypeFromEncodingCodeFS() throws IOException {
+        testReadPrimitiveTypeFromEncodingCode(true);
+    }
+
+    private void testReadPrimitiveTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        buffer.writeDouble(42.0);
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        buffer.writeDouble(43.0);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42.0, streamDecoder.readDouble(stream, streamDecoderState).shortValue(), 0.0);
+            assertEquals(43.0, streamDecoder.readDouble(stream, streamDecoderState, 42.0), 0.0);
+            assertNull(streamDecoder.readDouble(stream, streamDecoderState));
+            assertEquals(43.0, streamDecoder.readDouble(stream, streamDecoderState, 43.0), 0.0);
+        } else {
+            assertEquals(42.0, decoder.readDouble(buffer, decoderState).shortValue(), 0.0);
+            assertEquals(43.0, decoder.readDouble(buffer, decoderState, 42.0), 0.0);
+            assertNull(decoder.readDouble(buffer, decoderState));
+            assertEquals(43.0, decoder.readDouble(buffer, decoderState, 43.0), 0.0);
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.DOUBLE, (byte) new DoubleTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Double.class, new DoubleTypeEncoder().getTypeClass());
+        assertEquals(Double.class, new DoubleTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadDoubleFromEncodingCode() throws IOException {
+        testReadDoubleFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadDoubleFromEncodingCodeFS() throws IOException {
+        testReadDoubleFromEncodingCode(true);
+    }
+
+    private void testReadDoubleFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.DOUBLE);
+        buffer.writeDouble(42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readDouble(stream, streamDecoderState).intValue());
+        } else {
+            assertEquals(42, decoder.readDouble(buffer, decoderState).intValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeDouble(buffer, encoderState, Double.MAX_VALUE);
+            encoder.writeDouble(buffer, encoderState, 16.1);
+        }
+
+        double expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Double.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Double.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Double.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Double.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Double);
+
+        Double value = (Double) result;
+        assertEquals(expected, value.doubleValue(), 0.1f);
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Double[] source = new Double[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Double.valueOf((char) i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        double[] array = (double[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Double[] source = new Double[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        double[] array = (double[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/FloatTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/FloatTypeCodecTest.java
new file mode 100644
index 0000000..ae0c362
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/FloatTypeCodecTest.java
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.FloatTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.FloatTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class FloatTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readFloat(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readFloat(stream, streamDecoderState, 0f);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readFloat(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readFloat(buffer, decoderState, 0f);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadPrimitiveTypeFromEncodingCode() throws IOException {
+        testReadPrimitiveTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadPrimitiveTypeFromEncodingCodeFS() throws IOException {
+        testReadPrimitiveTypeFromEncodingCode(true);
+    }
+
+    private void testReadPrimitiveTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.FLOAT);
+        buffer.writeFloat(42.0f);
+        buffer.writeByte(EncodingCodes.FLOAT);
+        buffer.writeFloat(43.0f);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42f, streamDecoder.readFloat(stream, streamDecoderState).shortValue(), 0.0f);
+            assertEquals(43f, streamDecoder.readFloat(stream, streamDecoderState, (short) 42), 0.0f);
+            assertNull(streamDecoder.readFloat(stream, streamDecoderState));
+            assertEquals(43f, streamDecoder.readFloat(stream, streamDecoderState, 43f), 0.0f);
+        } else {
+            assertEquals(42f, decoder.readFloat(buffer, decoderState).shortValue(), 0.0f);
+            assertEquals(43f, decoder.readFloat(buffer, decoderState, (short) 42), 0.0f);
+            assertNull(decoder.readFloat(buffer, decoderState));
+            assertEquals(43f, decoder.readFloat(buffer, decoderState, 43f), 0.0f);
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeArrayOfPrimitiveFlosts() throws IOException {
+        doTestEncodeAndDecodeArrayOfPrimitiveFlosts(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeArrayOfPrimitiveFlostsFromStream() throws IOException {
+        doTestEncodeAndDecodeArrayOfPrimitiveFlosts(true);
+    }
+
+    private void doTestEncodeAndDecodeArrayOfPrimitiveFlosts(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        float[] floats = new float[] { 0.1f, 0.2f, 1.1f, 1.2f };
+
+        encoder.writeArray(buffer, encoderState, floats);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        float[] resultArray = (float[]) result;
+
+        assertArrayEquals(floats, resultArray);
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.FLOAT, (byte) new FloatTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Float.class, new FloatTypeEncoder().getTypeClass());
+        assertEquals(Float.class, new FloatTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadFloatFromEncodingCode() throws IOException {
+        testReadFloatFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFloatFromEncodingCodeFS() throws IOException {
+        testReadFloatFromEncodingCode(true);
+    }
+
+    private void testReadFloatFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.FLOAT);
+        buffer.writeFloat(42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readFloat(stream, streamDecoderState).intValue());
+        } else {
+            assertEquals(42, decoder.readFloat(buffer, decoderState).intValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeFloat(buffer, encoderState, Float.MAX_VALUE);
+            encoder.writeFloat(buffer, encoderState, 16.1f);
+        }
+
+        float expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Float.class, typeDecoder.getTypeClass());
+            typeDecoder.skipValue(buffer, decoderState);
+            typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Float.class, typeDecoder.getTypeClass());
+            typeDecoder.skipValue(buffer, decoderState);
+        }
+
+        final Object result = decoder.readObject(buffer, decoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Float);
+
+        Float value = (Float) result;
+        assertEquals(expected, value.floatValue(), 0.1f);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeFloat(buffer, encoderState, Float.MAX_VALUE);
+            encoder.writeFloat(buffer, encoderState, 16.1f);
+        }
+
+        float expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Float.class, typeDecoder.getTypeClass());
+            typeDecoder.skipValue(stream, streamDecoderState);
+            typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Float.class, typeDecoder.getTypeClass());
+            typeDecoder.skipValue(stream, streamDecoderState);
+        }
+
+        final Object result = streamDecoder.readObject(stream, streamDecoderState);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Float);
+
+        Float value = (Float) result;
+        assertEquals(expected, value.floatValue(), 0.1f);
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Float[] source = new Float[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = random.nextFloat();
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        float[] array = (float[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Float[] source = new Float[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        float[] array = (float[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/IntegerTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/IntegerTypeCodecTest.java
new file mode 100644
index 0000000..4d60d90
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/IntegerTypeCodecTest.java
@@ -0,0 +1,366 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Integer32TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Integer8TypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.IntegerTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class IntegerTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readInteger(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readInteger(stream, streamDecoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readInteger(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readInteger(buffer, decoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadUByteFromEncodingCode() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.INT);
+        buffer.writeInt(42);
+        buffer.writeByte(EncodingCodes.INT);
+        buffer.writeInt(44);
+        buffer.writeByte(EncodingCodes.SMALLINT);
+        buffer.writeByte(43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertEquals(42, decoder.readInteger(buffer, decoderState).intValue());
+        assertEquals(44, decoder.readInteger(buffer, decoderState, 42));
+        assertEquals(43, decoder.readInteger(buffer, decoderState, 42));
+        assertNull(decoder.readInteger(buffer, decoderState));
+        assertEquals(42, decoder.readInteger(buffer, decoderState, 42));
+    }
+
+    @Test
+    public void testReadUByteFromEncodingCodeFromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.INT);
+        buffer.writeInt(42);
+        buffer.writeByte(EncodingCodes.INT);
+        buffer.writeInt(44);
+        buffer.writeByte(EncodingCodes.SMALLINT);
+        buffer.writeByte(43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        assertEquals(42, streamDecoder.readInteger(stream, streamDecoderState).intValue());
+        assertEquals(44, streamDecoder.readInteger(stream, streamDecoderState, 42));
+        assertEquals(43, streamDecoder.readInteger(stream, streamDecoderState, 42));
+        assertNull(streamDecoder.readInteger(stream, streamDecoderState));
+        assertEquals(42, streamDecoder.readInteger(stream, streamDecoderState, 42));
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.INT, (byte) new Integer32TypeDecoder().getTypeCode());
+        assertEquals(EncodingCodes.SMALLINT, (byte) new Integer8TypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Integer.class, new IntegerTypeEncoder().getTypeClass());
+        assertEquals(Integer.class, new Integer8TypeDecoder().getTypeClass());
+        assertEquals(Integer.class, new Integer32TypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadIntegerFromEncodingCodeInt() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.INT);
+        buffer.writeInt(42);
+
+        assertEquals(42, decoder.readInteger(buffer, decoderState).intValue());
+    }
+
+    @Test
+    public void testReadIntegerFromEncodingCodeSmallInt() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.SMALLINT);
+        buffer.writeByte(42);
+
+        assertEquals(42, decoder.readInteger(buffer, decoderState).intValue());
+    }
+
+    @Test
+    public void testReadIntegerFromEncodingCodeIntFromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.INT);
+        buffer.writeInt(42);
+
+        assertEquals(42, streamDecoder.readInteger(stream, streamDecoderState).intValue());
+    }
+
+    @Test
+    public void testReadIntegerFromEncodingCodeSmallIntFromStream() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.SMALLINT);
+        buffer.writeByte(42);
+
+        assertEquals(42, streamDecoder.readInteger(stream, streamDecoderState).intValue());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeInteger(buffer, encoderState, Integer.MAX_VALUE);
+            encoder.writeInteger(buffer, encoderState, 16);
+        }
+
+        int expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Integer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Integer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Integer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Integer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Integer);
+
+        Integer value = (Integer) result;
+        assertEquals(expected, value.intValue());
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        doTestArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFromStream() throws IOException {
+        doTestArrayOfObjects(true);
+    }
+
+    protected void doTestArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        Integer[] source = new Integer[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = random.nextInt();
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        int[] array = (int[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Integer[] source = new Integer[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        int[] array = (int[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testReadIntegerArrayInt32() throws IOException {
+        doTestReadIntegerArray(EncodingCodes.INT, false);
+    }
+
+    @Test
+    public void testReadIntegerArrayInt32FromStream() throws IOException {
+        doTestReadIntegerArray(EncodingCodes.INT, true);
+    }
+
+    @Test
+    public void testReadIntegerArrayInt8() throws IOException {
+        doTestReadIntegerArray(EncodingCodes.SMALLINT, false);
+    }
+
+    @Test
+    public void testReadIntegerArrayInt8FromStream() throws IOException {
+        doTestReadIntegerArray(EncodingCodes.SMALLINT, true);
+    }
+
+    public void doTestReadIntegerArray(byte encoding, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        if (encoding == EncodingCodes.INT) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(17);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.INT);
+            buffer.writeInt(1);   // [0]
+            buffer.writeInt(2);   // [1]
+        } else if (encoding == EncodingCodes.SMALLINT) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(11);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.SMALLINT);
+            buffer.writeByte(1);   // [0]
+            buffer.writeByte(2);   // [1]
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        int[] array = (int[]) result;
+
+        assertEquals(2, array.length);
+        assertEquals(1, array[0]);
+        assertEquals(2, array[1]);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ListTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ListTypeCodecTest.java
new file mode 100644
index 0000000..ab54654
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ListTypeCodecTest.java
@@ -0,0 +1,579 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.mockito.Mockito;
+
+/**
+ * Test for the Proton List encoder / decoder
+ */
+@Timeout(20)
+public class ListTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readList(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readList(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte(4);
+        buffer.writeByte(2);
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte(1);
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte(2);
+
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt(4);
+        buffer.writeInt(2);
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte(1);
+        buffer.writeByte(EncodingCodes.BYTE);
+        buffer.writeByte(2);
+
+        List<Byte> expected = new ArrayList<>();
+
+        expected.add(Byte.valueOf((byte) 1));
+        expected.add(Byte.valueOf((byte) 2));
+
+        if (fromStream) {
+            assertNull(streamDecoder.readList(stream, streamDecoderState));
+            assertEquals(Collections.EMPTY_LIST, streamDecoder.readList(stream, streamDecoderState));
+            assertEquals(expected, streamDecoder.readList(stream, streamDecoderState));
+            assertEquals(expected, streamDecoder.readList(stream, streamDecoderState));
+        } else {
+            assertNull(decoder.readList(buffer, decoderState));
+            assertEquals(Collections.EMPTY_LIST, decoder.readList(buffer, decoderState));
+            assertEquals(expected, decoder.readList(buffer, decoderState));
+            assertEquals(expected, decoder.readList(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfSymbolLists() throws IOException {
+        doTestDecodeSymbolListSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfSymbolLists() throws IOException {
+        doTestDecodeSymbolListSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfSymbolListsFromStream() throws IOException {
+        doTestDecodeSymbolListSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfSymbolListsFromStream() throws IOException {
+        doTestDecodeSymbolListSeries(LARGE_SIZE, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void doTestDecodeSymbolListSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<Object> list = new ArrayList<>();
+
+        for (int i = 0; i < 50; ++i) {
+            list.add(Symbol.valueOf(String.valueOf(i)));
+        }
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, list);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof List);
+
+            List<Object> resultList = (List<Object>) result;
+
+            assertEquals(list.size(), resultList.size());
+        }
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfLists() throws IOException {
+        doTestDecodeListSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfLists() throws IOException {
+        doTestDecodeListSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfListsFS() throws IOException {
+        doTestDecodeListSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfListsFS() throws IOException {
+        doTestDecodeListSeries(LARGE_SIZE, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void doTestDecodeListSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<Object> list = new ArrayList<>();
+
+        Date timeNow = new Date(System.currentTimeMillis());
+
+        list.add("ID:Message-1:1:1:0");
+        list.add(new Binary(new byte[1]));
+        list.add("queue:work");
+        list.add(Symbol.valueOf("text/UTF-8"));
+        list.add(Symbol.valueOf("text"));
+        list.add(timeNow);
+        list.add(UnsignedInteger.valueOf(1));
+        list.add(UUID.randomUUID());
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, list);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof List);
+
+            List<Object> resultList = (List<Object>) result;
+
+            assertEquals(list.size(), resultList.size());
+        }
+    }
+
+    @Test
+    public void testArrayOfListsOfUUIDs() throws IOException {
+        doTestArrayOfListsOfUUIDs(false);
+    }
+
+    @Test
+    public void testArrayOfListsOfUUIDsFromStream() throws IOException {
+        doTestArrayOfListsOfUUIDs(true);
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    private void doTestArrayOfListsOfUUIDs(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ArrayList<UUID>[] source = new ArrayList[2];
+        for (int i = 0; i < source.length; ++i) {
+            source[i] = new ArrayList<>(3);
+            source[i].add(UUID.randomUUID());
+            source[i].add(UUID.randomUUID());
+            source[i].add(UUID.randomUUID());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        List[] list = (List[]) result;
+        assertEquals(source.length, list.length);
+
+        for (int i = 0; i < list.length; ++i) {
+            assertEquals(source[i], list[i]);
+        }
+    }
+
+    @Test
+    public void testCountExceedsRemainingDetectedList32() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt(8);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testCountExceedsRemainingDetectedList8() throws IOException {
+        testCountExceedsRemainingDetectedList8(false);
+    }
+
+    @Test
+    public void testCountExceedsRemainingDetectedList8FS() throws IOException {
+        testCountExceedsRemainingDetectedList8(true);
+    }
+
+    private void testCountExceedsRemainingDetectedList8(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte(4);
+        buffer.writeByte(Byte.MAX_VALUE);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        }
+    }
+
+    @Test
+    public void testDecodeEmptyList() throws IOException {
+        testDecodeEmptyList(false);
+    }
+
+    @Test
+    public void testDecodeEmptyListFS() throws IOException {
+        testDecodeEmptyList(true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void testDecodeEmptyList(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.LIST0);
+
+        final Object result;
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.peekNextTypeDecoder(stream, streamDecoderState);
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(EncodingCodes.LIST0 & 0xff, ((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode());
+
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.peekNextTypeDecoder(buffer, decoderState);
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(EncodingCodes.LIST0 & 0xff, ((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode());
+
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof List);
+
+        List<UUID> value = (List<UUID>) result;
+        assertEquals(0, value.size());
+    }
+
+    @Test
+    public void testEncodeEmptyListIsList0() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeList(buffer, encoderState, new ArrayList<>());
+
+        assertEquals(1, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.LIST0, buffer.readByte());
+    }
+
+    @Test
+    public void testDecodeFailsEarlyOnInvliadLengthList8() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte(255);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertEquals(List.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.readValue(buffer, decoderState);
+            fail("Should not be able to read list with length greater than readable bytes");
+        } catch (IllegalArgumentException ex) {}
+
+        assertEquals(2, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testDecodeFailsEarlyOnInvliadLengthList32() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertEquals(List.class, typeDecoder.getTypeClass());
+
+        try {
+            typeDecoder.readValue(buffer, decoderState);
+            fail("Should not be able to read list with length greater than readable bytes");
+        } catch (IllegalArgumentException ex) {}
+
+        assertEquals(5, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testDecodeFailsEarlyOnInvliadElementCountForList8() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.LIST8);
+        buffer.writeByte(1);
+        buffer.writeByte(255);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertEquals(List.class, typeDecoder.getTypeClass());
+        assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+        assertEquals(EncodingCodes.LIST8 & 0xff, ((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode());
+
+        try {
+            typeDecoder.readValue(buffer, decoderState);
+            fail("Should not be able to read list with length greater than readable bytes");
+        } catch (IllegalArgumentException ex) {}
+
+        assertEquals(3, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testDecodeFailsEarlyOnInvliadElementLengthList32() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        buffer.writeByte(EncodingCodes.LIST32);
+        buffer.writeInt(2);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+        assertEquals(List.class, typeDecoder.getTypeClass());
+        assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+        assertEquals(EncodingCodes.LIST32 & 0xff, ((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode());
+
+        try {
+            typeDecoder.readValue(buffer, decoderState);
+            fail("Should not be able to read list with length greater than readable bytes");
+        } catch (IllegalArgumentException ex) {}
+
+        assertEquals(9, buffer.getReadIndex());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<UUID> skip = new ArrayList<>();
+        for (int i = 0; i < 10; ++i) {
+            skip.add(UUID.randomUUID());
+        }
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeList(buffer, encoderState, skip);
+        }
+
+        List<UUID> expected = new ArrayList<>();
+        expected.add(UUID.randomUUID());
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(List.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(List.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof List);
+
+        List<UUID> value = (List<UUID>) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testEncodeListWithUnknownEntryType() throws Exception {
+        List<Object> list = new ArrayList<>();
+        list.add(new MyUnknownTestType());
+
+        doTestEncodeListWithUnknownEntryTypeTestImpl(list);
+    }
+
+    @Test
+    public void testEncodeSubListWithUnknownEntryType() throws Exception {
+        List<Object> subList = new ArrayList<>();
+        subList.add(new MyUnknownTestType());
+
+        List<Object> list = new ArrayList<>();
+        list.add(subList);
+
+        doTestEncodeListWithUnknownEntryTypeTestImpl(list);
+    }
+
+    @Test
+    public void testStreamSkipOfEncodingHandlesIOException() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        List<UUID> skip = new ArrayList<>();
+        for (int i = 0; i < 10; ++i) {
+            skip.add(UUID.randomUUID());
+        }
+
+        encoder.writeList(buffer, encoderState, skip);
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertEquals(List.class, typeDecoder.getTypeClass());
+
+        stream = Mockito.spy(stream);
+
+        Mockito.when(stream.skip(Mockito.anyLong())).thenThrow(EOFException.class);
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+            fail("Expected an exception on skip of encoded list failure.");
+        } catch (DecodeException dex) {}
+    }
+
+    private void doTestEncodeListWithUnknownEntryTypeTestImpl(List<Object> list) {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        try {
+            encoder.writeObject(buffer, encoderState, list);
+            fail("Expected exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            assertThat(iae.getMessage(), containsString("Cannot find encoder for type"));
+            assertThat(iae.getMessage(), containsString(MyUnknownTestType.class.getSimpleName()));
+        }
+    }
+
+    private static class MyUnknownTestType {
+
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/LongTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/LongTypeCodecTest.java
new file mode 100644
index 0000000..77db9a5
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/LongTypeCodecTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.Long8TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.LongTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.LongTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class LongTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readLong(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readLong(stream, streamDecoderState, 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readLong(stream, streamDecoderState, 0l);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readLong(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readLong(buffer, decoderState, 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readLong(buffer, decoderState, 0l);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.LONG);
+        buffer.writeLong(42);
+        buffer.writeByte(EncodingCodes.LONG);
+        buffer.writeLong(44);
+        buffer.writeByte(EncodingCodes.SMALLLONG);
+        buffer.writeByte(43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readLong(stream, streamDecoderState).intValue());
+            assertEquals(44, streamDecoder.readLong(stream, streamDecoderState, 42));
+            assertEquals(43, streamDecoder.readLong(stream, streamDecoderState, 42));
+            assertNull(streamDecoder.readLong(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readLong(stream, streamDecoderState, 42l));
+        } else {
+            assertEquals(42, decoder.readLong(buffer, decoderState).intValue());
+            assertEquals(44, decoder.readLong(buffer, decoderState, 42));
+            assertEquals(43, decoder.readLong(buffer, decoderState, 42));
+            assertNull(decoder.readLong(buffer, decoderState));
+            assertEquals(42, decoder.readLong(buffer, decoderState, 42l));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.LONG, (byte) new LongTypeDecoder().getTypeCode());
+        assertEquals(EncodingCodes.SMALLLONG, (byte) new Long8TypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Long.class, new LongTypeEncoder().getTypeClass());
+        assertEquals(Long.class, new Long8TypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadLongFromEncodingCodeLong() throws IOException {
+        testReadLongFromEncodingCodeLong(false);
+    }
+
+    @Test
+    public void testReadLongFromEncodingCodeLongFS() throws IOException {
+        testReadLongFromEncodingCodeLong(true);
+    }
+
+    private void testReadLongFromEncodingCodeLong(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.LONG);
+        buffer.writeLong(42);
+
+        if (fromStream) {
+            assertEquals(42l, streamDecoder.readLong(stream, streamDecoderState).longValue());
+        } else {
+            assertEquals(42l, decoder.readLong(buffer, decoderState).longValue());
+        }
+    }
+
+    @Test
+    public void testReadLongFromEncodingCodeSmallLong() throws IOException {
+        testReadLongFromEncodingCodeSmallLong(false);
+    }
+
+    @Test
+    public void testReadLongFromEncodingCodeSmallLongFS() throws IOException {
+        testReadLongFromEncodingCodeSmallLong(true);
+    }
+
+    private void testReadLongFromEncodingCodeSmallLong(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.SMALLLONG);
+        buffer.writeByte(42);
+
+        if (fromStream) {
+            assertEquals(42l, streamDecoder.readLong(stream, streamDecoderState).longValue());
+        } else {
+            assertEquals(42l, decoder.readLong(buffer, decoderState).longValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeLong(buffer, encoderState, Long.MAX_VALUE);
+            encoder.writeLong(buffer, encoderState, 16);
+        }
+
+        long expected = 42l;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Long);
+
+        Long value = (Long) result;
+        assertEquals(expected, value.intValue());
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int size = 10;
+
+        Long[] source = new Long[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Long.valueOf(random.nextLong());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        long[] array = (long[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Long[] source = new Long[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        long[] array = (long[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testReadLongArray() throws IOException {
+        doTestReadLongArray(EncodingCodes.LONG, false);
+    }
+
+    @Test
+    public void testReadLongArrayFromStream() throws IOException {
+        doTestReadLongArray(EncodingCodes.LONG, true);
+    }
+
+    @Test
+    public void testReadSmallLongArray() throws IOException {
+        doTestReadLongArray(EncodingCodes.SMALLLONG, false);
+    }
+
+    @Test
+    public void testReadSmallLongArrayFromStream() throws IOException {
+        doTestReadLongArray(EncodingCodes.SMALLLONG, true);
+    }
+
+    public void doTestReadLongArray(byte encoding, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        if (encoding == EncodingCodes.LONG) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(25);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.LONG);
+            buffer.writeLong(1l);   // [0]
+            buffer.writeLong(2l);   // [1]
+        } else if (encoding == EncodingCodes.SMALLLONG) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(11);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.SMALLLONG);
+            buffer.writeByte(1);   // [0]
+            buffer.writeByte(2);   // [1]
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        long[] array = (long[]) result;
+
+        assertEquals(2, array.length);
+        assertEquals(1, array[0]);
+        assertEquals(2, array[1]);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/MapTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/MapTypeCodecTest.java
new file mode 100644
index 0000000..6abc6f2
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/MapTypeCodecTest.java
@@ -0,0 +1,504 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class MapTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readMap(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readMap(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfMaps() throws IOException {
+        doTestDecodeMapSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfMaps() throws IOException {
+        doTestDecodeMapSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfMapsFromStream() throws IOException {
+        doTestDecodeMapSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfMapsFromStream() throws IOException {
+        doTestDecodeMapSeries(LARGE_SIZE, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void doTestDecodeMapSeries(int size, boolean fromStream) throws IOException {
+        String myBoolKey = "myBool";
+        boolean myBool = true;
+        String myByteKey = "myByte";
+        byte myByte = 4;
+        String myBytesKey = "myBytes";
+        byte[] myBytes = myBytesKey.getBytes();
+        String myCharKey = "myChar";
+        char myChar = 'd';
+        String myDoubleKey = "myDouble";
+        double myDouble = 1234567890123456789.1234;
+        String myFloatKey = "myFloat";
+        float myFloat = 1.1F;
+        String myIntKey = "myInt";
+        int myInt = Integer.MAX_VALUE;
+        String myLongKey = "myLong";
+        long myLong = Long.MAX_VALUE;
+        String myShortKey = "myShort";
+        short myShort = 25;
+        String myStringKey = "myString";
+        String myString = myStringKey;
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(myBoolKey, myBool);
+        map.put(myByteKey, myByte);
+        map.put(myBytesKey, new Binary(myBytes));
+        map.put(myCharKey, myChar);
+        map.put(myDoubleKey, myDouble);
+        map.put(myFloatKey, myFloat);
+        map.put(myIntKey, myInt);
+        map.put(myLongKey, myLong);
+        map.put(myShortKey, myShort);
+        map.put(myStringKey, myString);
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, map);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Map);
+
+            Map<String, Object> resultMap = (Map<String, Object>) result;
+
+            assertEquals(map.size(), resultMap.size());
+        }
+    }
+
+    @Test
+    public void testArrayOfMApsOfStringToUUIDs() throws IOException {
+        testArrayOfMApsOfStringToUUIDs(false);
+    }
+
+    @Test
+    public void testArrayOfMApsOfStringToUUIDsFS() throws IOException {
+        testArrayOfMApsOfStringToUUIDs(true);
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    private void testArrayOfMApsOfStringToUUIDs(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<String, UUID>[] source = new LinkedHashMap[2];
+        for (int i = 0; i < source.length; ++i) {
+            source[i] = new LinkedHashMap<>();
+            source[i].put("1", UUID.randomUUID());
+            source[i].put("2", UUID.randomUUID());
+            source[i].put("3", UUID.randomUUID());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Map[] map = (Map[]) result;
+        assertEquals(source.length, map.length);
+
+        for (int i = 0; i < map.length; ++i) {
+            assertEquals(source[i], map[i]);
+        }
+    }
+
+    @Test
+    public void testMapOfArraysOfUUIDsIndexedByString() throws IOException {
+        testMapOfArraysOfUUIDsIndexedByString(false);
+    }
+
+    @Test
+    public void testMapOfArraysOfUUIDsIndexedByStringFS() throws IOException {
+        testMapOfArraysOfUUIDsIndexedByString(true);
+    }
+
+    @SuppressWarnings({ "unchecked" })
+    private void testMapOfArraysOfUUIDsIndexedByString(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UUID[] element1 = new UUID[] { UUID.randomUUID() };
+        UUID[] element2 = new UUID[] { UUID.randomUUID(), UUID.randomUUID() };
+        UUID[] element3 = new UUID[] { UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID() };
+
+        UUID[][] expected = new UUID[][] { element1, element2, element3 };
+
+        Map<String, UUID[]> source = new LinkedHashMap<>();
+        source.put("1", element1);
+        source.put("2", element2);
+        source.put("3", element3);
+
+        encoder.writeMap(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Map);
+
+        Map<String, UUID[]> map = (Map<String, UUID[]>) result;
+        assertEquals(source.size(), map.size());
+
+        for (int i = 1; i <= map.size(); ++i) {
+            Object entry = map.get(Integer.toString(i));
+            assertNotNull(entry);
+            assertTrue(entry.getClass().isArray());
+            UUID[] uuids = (UUID[]) entry;
+            assertEquals(i, uuids.length);
+            assertArrayEquals(expected[i - 1], uuids);
+        }
+    }
+
+    @Test
+    public void testSizeToLargeValidationMAP32() throws IOException {
+        dotestSizeToLargeValidation(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSizeToLargeValidationMAP8() throws IOException {
+        dotestSizeToLargeValidation(EncodingCodes.MAP8, true);
+    }
+
+    private void dotestSizeToLargeValidation(byte encodingCode, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(encodingCode);
+        if (encodingCode == EncodingCodes.MAP32) {
+            buffer.writeInt(Integer.MAX_VALUE);
+            buffer.writeInt(4);
+        } else {
+            buffer.writeByte(Byte.MAX_VALUE);
+            buffer.writeByte(4);
+        }
+        buffer.writeByte(EncodingCodes.STR8);
+        buffer.writeByte(4);
+        buffer.writeBytes("test".getBytes(StandardCharsets.UTF_8));
+        buffer.writeByte(EncodingCodes.STR8);
+        buffer.writeByte(5);
+        buffer.writeBytes("value".getBytes(StandardCharsets.UTF_8));
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.peekNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Map.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), encodingCode & 0xFF);
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.peekNextTypeDecoder(buffer, decoderState);
+            assertEquals(Map.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), encodingCode & 0xFF);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        }
+    }
+
+    @Test
+    public void testOddElementCountDetectedMAP32() throws IOException {
+        doTestOddElementCountDetected(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testOddElementCountDetectedMAP8() throws IOException {
+        doTestOddElementCountDetected(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testOddElementCountDetectedMAP32FS() throws IOException {
+        doTestOddElementCountDetected(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testOddElementCountDetectedMAP8FS() throws IOException {
+        doTestOddElementCountDetected(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestOddElementCountDetected(byte encodingCode, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(encodingCode);
+        if (encodingCode == EncodingCodes.MAP32) {
+            buffer.writeInt(17);
+            buffer.writeInt(1);
+        } else {
+            buffer.writeByte(17);
+            buffer.writeByte(1);
+        }
+        buffer.writeByte(EncodingCodes.STR8);
+        buffer.writeByte(4);
+        buffer.writeBytes("test".getBytes(StandardCharsets.UTF_8));
+        buffer.writeByte(EncodingCodes.STR8);
+        buffer.writeByte(5);
+        buffer.writeBytes("value".getBytes(StandardCharsets.UTF_8));
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<String, UUID> skip = new HashMap<>();
+        for (int i = 0; i < 10; ++i) {
+            skip.put(UUID.randomUUID().toString(), UUID.randomUUID());
+        }
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeMap(buffer, encoderState, skip);
+        }
+
+        Map<String, UUID> expected = new LinkedHashMap<>();
+        expected.put(UUID.randomUUID().toString(), UUID.randomUUID());
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Map.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Map.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Map);
+
+        Map<String, UUID> value = (Map<String, UUID>) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testEncodeMapWithUnknownEntryValueType() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("unknown", new MyUnknownTestType());
+
+        doTestEncodeMapWithUnknownEntryValueTypeTestImpl(map);
+    }
+
+    @Test
+    public void testEncodeSubMapWithUnknownEntryValueType() throws Exception {
+        Map<String, Object> subMap = new HashMap<>();
+        subMap.put("unknown", new MyUnknownTestType());
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("submap", subMap);
+
+        doTestEncodeMapWithUnknownEntryValueTypeTestImpl(map);
+    }
+
+    private void doTestEncodeMapWithUnknownEntryValueTypeTestImpl(Map<String, Object> map) {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        try {
+            encoder.writeMap(buffer, encoderState, map);
+            fail("Expected exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            assertThat(iae.getMessage(), containsString("Cannot find encoder for type"));
+            assertThat(iae.getMessage(), containsString(MyUnknownTestType.class.getSimpleName()));
+        }
+    }
+
+    @Test
+    public void testEncodeMapWithUnknownEntryKeyType() throws Exception {
+        Map<Object, String> map = new HashMap<>();
+        map.put(new MyUnknownTestType(), "unknown");
+
+        doTestEncodeMapWithUnknownEntryKeyTypeTestImpl(map);
+    }
+
+    @Test
+    public void testEncodeSubMapWithUnknownEntryKeyType() throws Exception {
+        Map<Object, String> subMap = new HashMap<>();
+        subMap.put(new MyUnknownTestType(), "unknown");
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("submap", subMap);
+
+        doTestEncodeMapWithUnknownEntryKeyTypeTestImpl(map);
+    }
+
+    @Test
+    public void testStreamSkipOfListEncodingHandlesIOException() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<String, UUID> skip = new HashMap<>();
+        for (int i = 0; i < 10; ++i) {
+            skip.put(UUID.randomUUID().toString(), UUID.randomUUID());
+        }
+
+        encoder.writeMap(buffer, encoderState, skip);
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertEquals(Map.class, typeDecoder.getTypeClass());
+
+        stream = Mockito.spy(stream);
+
+        Mockito.when(stream.skip(Mockito.anyLong())).thenThrow(EOFException.class);
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+            fail("Expected an exception on skip when it throws.");
+        } catch (DecodeException dex) {}
+    }
+
+    private void doTestEncodeMapWithUnknownEntryKeyTypeTestImpl(Map<?, ?> map) {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        try {
+            encoder.writeMap(buffer, encoderState, map);
+            fail("Expected exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            assertThat(iae.getMessage(), containsString("Cannot find encoder for type"));
+            assertThat(iae.getMessage(), containsString(MyUnknownTestType.class.getSimpleName()));
+        }
+    }
+
+    private static class MyUnknownTestType {
+
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/NullTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/NullTypeCodecTest.java
new file mode 100644
index 0000000..fb92ae7
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/NullTypeCodecTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.NullTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.NullTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class NullTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.NULL, new NullTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Void.class, new NullTypeEncoder().getTypeClass());
+        assertEquals(Void.class, new NullTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testWriteOfArrayThrowsException() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(1, 1);
+
+        try {
+            new NullTypeEncoder().writeArray(buffer, encoderState, new Object[1]);
+            fail("Null encoder cannot write array types");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testWriteRawOfArrayThrowsException() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(1, 1);
+
+        try {
+            new NullTypeEncoder().writeRawArray(buffer, encoderState, new Object[1]);
+            fail("Null encoder cannot write array types");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testReadNullDoesNotTouchBuffer() throws IOException {
+        testReadNullDoesNotTouchBuffer(false);
+    }
+
+    @Test
+    public void testReadNullDoesNotTouchBufferFS() throws IOException {
+        testReadNullDoesNotTouchBuffer(true);
+    }
+
+    private void testReadNullDoesNotTouchBuffer(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(1, 1);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertNull(streamDecoder.readObject(stream, streamDecoderState));
+        } else {
+            assertNull(decoder.readObject(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testSkipNullDoesNotTouchBuffer() throws IOException {
+        doTestSkipNullDoesNotTouchBuffer(false);
+    }
+
+    @Test
+    public void testSkipNullDoesNotTouchStream() throws IOException {
+        doTestSkipNullDoesNotTouchBuffer(true);
+    }
+
+    private void doTestSkipNullDoesNotTouchBuffer(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Void.class, typeDecoder.getTypeClass());
+            int index = buffer.getReadIndex();
+            typeDecoder.skipValue(stream, streamDecoderState);
+            assertEquals(index, buffer.getReadIndex());
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Void.class, typeDecoder.getTypeClass());
+            int index = buffer.getReadIndex();
+            typeDecoder.skipValue(buffer, decoderState);
+            assertEquals(index, buffer.getReadIndex());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ShortTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ShortTypeCodecTest.java
new file mode 100644
index 0000000..cb00fa9
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/ShortTypeCodecTest.java
@@ -0,0 +1,283 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.ShortTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.ShortTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class ShortTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readShort(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readShort(stream, streamDecoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readShort(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readShort(buffer, decoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.SHORT);
+        buffer.writeShort((short) 42);
+        buffer.writeByte(EncodingCodes.SHORT);
+        buffer.writeShort((short) 43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readShort(stream, streamDecoderState).shortValue());
+            assertEquals(43, streamDecoder.readShort(stream, streamDecoderState, (short) 42));
+            assertNull(streamDecoder.readShort(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readShort(stream, streamDecoderState, (short) 42));
+        } else {
+            assertEquals(42, decoder.readShort(buffer, decoderState).shortValue());
+            assertEquals(43, decoder.readShort(buffer, decoderState, (short) 42));
+            assertNull(decoder.readShort(buffer, decoderState));
+            assertEquals(42, decoder.readShort(buffer, decoderState, (short) 42));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.SHORT, (byte) new ShortTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Short.class, new ShortTypeEncoder().getTypeClass());
+        assertEquals(Short.class, new ShortTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadShortFromEncodingCode() throws IOException {
+        testReadShortFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadShortFromEncodingCodeFS() throws IOException {
+        testReadShortFromEncodingCode(true);
+    }
+
+    private void testReadShortFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.SHORT);
+        buffer.writeShort((short) 42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readShort(stream, streamDecoderState).intValue());
+        } else {
+            assertEquals(42, decoder.readShort(buffer, decoderState).intValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeShort(buffer, encoderState, Short.MAX_VALUE);
+            encoder.writeShort(buffer, encoderState, (short) 16);
+        }
+
+        short expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Short.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Short.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Short.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Short.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Short);
+
+        Short value = (Short) result;
+        assertEquals(expected, value.shortValue());
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int size = 10;
+
+        Short[] source = new Short[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Short.valueOf((short) random.nextInt(65535));
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        short[] array = (short[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Short[] source = new Short[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        short[] array = (short[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/StringTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/StringTypeCodecTest.java
new file mode 100644
index 0000000..03fdd13
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/StringTypeCodecTest.java
@@ -0,0 +1,582 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Character.UnicodeBlock;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class StringTypeCodecTest extends CodecTestSupport {
+
+    private static final List<String> TEST_DATA = generateTestData();
+
+    private final String SMALL_STRING_VALUIE = "Small String";
+    private final String LARGE_STRING_VALUIE = "Large String: " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog.";
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readString(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readString(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        testReadFromNullEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromNullEncodingCodeFS() throws IOException {
+        testReadFromNullEncodingCode(true);
+    }
+
+    private void testReadFromNullEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertNull(streamDecoder.readString(stream, streamDecoderState));
+        } else {
+            assertNull(decoder.readString(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testEncodeSmallString() throws IOException {
+        doTestEncodeDecode(SMALL_STRING_VALUIE, false);
+    }
+
+    @Test
+    public void testEncodeLargeString() throws IOException {
+        doTestEncodeDecode(LARGE_STRING_VALUIE, false);
+    }
+
+    @Test
+    public void testEncodeEmptyString() throws IOException {
+        doTestEncodeDecode("", false);
+    }
+
+    @Test
+    public void testEncodeNullString() throws IOException {
+        doTestEncodeDecode(null, false);
+    }
+
+    @Test
+    public void testEncodeSmallStringFS() throws IOException {
+        doTestEncodeDecode(SMALL_STRING_VALUIE, true);
+    }
+
+    @Test
+    public void testEncodeLargeStringFS() throws IOException {
+        doTestEncodeDecode(LARGE_STRING_VALUIE, true);
+    }
+
+    @Test
+    public void testEncodeEmptyStringFS() throws IOException {
+        doTestEncodeDecode("", true);
+    }
+
+    @Test
+    public void testEncodeNullStringFS() throws IOException {
+        doTestEncodeDecode(null, true);
+    }
+
+    private void doTestEncodeDecode(String value, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        if (value != null) {
+            assertNotNull(result);
+            assertTrue(result instanceof String);
+        } else {
+            assertNull(result);
+        }
+
+        assertEquals(value, result);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfStrings() throws IOException {
+        doTestDecodeStringSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfStrings() throws IOException {
+        doTestDecodeStringSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfStringsFS() throws IOException {
+        doTestDecodeStringSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfStringsFS() throws IOException {
+        doTestDecodeStringSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeStringSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeString(buffer, encoderState, LARGE_STRING_VALUIE);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof String);
+            assertEquals(LARGE_STRING_VALUIE, result);
+        }
+    }
+
+    @Test
+    public void testDecodeStringOfZeroLengthWithLargeEncoding() throws IOException {
+        doTestDecodeStringOfZeroLengthWithGivenEncoding(EncodingCodes.STR32, false);
+    }
+
+    @Test
+    public void testDecodeStringOfZeroLengthWithSmallEncoding() throws IOException {
+        doTestDecodeStringOfZeroLengthWithGivenEncoding(EncodingCodes.STR8, false);
+    }
+
+    @Test
+    public void testDecodeStringOfZeroLengthWithLargeEncodingFS() throws IOException {
+        doTestDecodeStringOfZeroLengthWithGivenEncoding(EncodingCodes.STR32, true);
+    }
+
+    @Test
+    public void testDecodeStringOfZeroLengthWithSmallEncodingFS() throws IOException {
+        doTestDecodeStringOfZeroLengthWithGivenEncoding(EncodingCodes.STR8, true);
+    }
+
+    private void doTestDecodeStringOfZeroLengthWithGivenEncoding(byte encodingCode, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        // Manually encode the type we want.
+        if (encodingCode == EncodingCodes.STR32) {
+            buffer.writeByte(EncodingCodes.STR32);
+            buffer.writeInt(0);
+        } else {
+            buffer.writeByte(EncodingCodes.STR8);
+            buffer.writeByte(0);
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.peekNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(String.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), encodingCode & 0xFF);
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.peekNextTypeDecoder(buffer, decoderState);
+            assertEquals(String.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), encodingCode & 0xFF);
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertEquals("", result);
+    }
+
+    @Test
+    public void testEncodeAndDecodeComplexStrings() throws IOException {
+        testEncodeAndDecodeComplexStrings(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeComplexStringsFS() throws IOException {
+        testEncodeAndDecodeComplexStrings(true);
+    }
+
+    private void testEncodeAndDecodeComplexStrings(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (final String input : TEST_DATA) {
+            encoder.writeString(buffer, encoderState, input);
+
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            buffer.clear();
+
+            assertEquals(input, result);
+        }
+    }
+
+    @Test
+    public void testEncodedSizeExceedsRemainingDetectedStr32() throws IOException {
+        testEncodedSizeExceedsRemainingDetectedStr32(false);
+    }
+
+    @Test
+    public void testEncodedSizeExceedsRemainingDetectedStr32FS() throws IOException {
+        testEncodedSizeExceedsRemainingDetectedStr32(true);
+    }
+
+    private void testEncodedSizeExceedsRemainingDetectedStr32(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.STR32);
+        buffer.writeInt(8);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        }
+    }
+
+    @Test
+    public void testEncodedSizeExceedsRemainingDetectedStr8() throws IOException {
+        testEncodedSizeExceedsRemainingDetectedStr8(false);
+    }
+
+    @Test
+    public void testEncodedSizeExceedsRemainingDetectedStr8FS() throws IOException {
+        testEncodedSizeExceedsRemainingDetectedStr8(true);
+    }
+
+    private void testEncodedSizeExceedsRemainingDetectedStr8(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.STR8);
+        buffer.writeByte(4);
+        buffer.writeByte(Byte.MAX_VALUE);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("should throw an IllegalArgumentException");
+            } catch (IllegalArgumentException iae) {}
+        }
+    }
+
+    //----- Test support for string encodings --------------------------------//
+
+    private static List<String> generateTestData() {
+        return new LinkedList<String>() {
+            private static final long serialVersionUID = 7331717267070233454L;
+
+            {
+                // non-surrogate pair blocks
+                addAll(getAllStringsFromUnicodeBlocks(UnicodeBlock.BASIC_LATIN,
+                                                      UnicodeBlock.LATIN_1_SUPPLEMENT,
+                                                      UnicodeBlock.GREEK,
+                                                      UnicodeBlock.LETTERLIKE_SYMBOLS));
+                // blocks with surrogate pairs
+                addAll(getAllStringsFromUnicodeBlocks(UnicodeBlock.LINEAR_B_SYLLABARY,
+                                                      UnicodeBlock.MISCELLANEOUS_SYMBOLS_AND_PICTOGRAPHS,
+                                                      UnicodeBlock.MUSICAL_SYMBOLS,
+                                                      UnicodeBlock.EMOTICONS,
+                                                      UnicodeBlock.PLAYING_CARDS,
+                                                      UnicodeBlock.BOX_DRAWING,
+                                                      UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS,
+                                                      UnicodeBlock.PRIVATE_USE_AREA,
+                                                      UnicodeBlock.SUPPLEMENTARY_PRIVATE_USE_AREA_A,
+                                                      UnicodeBlock.SUPPLEMENTARY_PRIVATE_USE_AREA_B));
+
+                // some additional combinations of characters that could cause problems to the encoder
+                String[] boxDrawing = getAllStringsFromUnicodeBlocks(UnicodeBlock.BOX_DRAWING).toArray(new String[0]);
+                String[] halfFullWidthForms = getAllStringsFromUnicodeBlocks(UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS).toArray(new String[0]);
+                for (int i = 0; i < halfFullWidthForms.length; i++) {
+                    add(halfFullWidthForms[i] + boxDrawing[i % boxDrawing.length]);
+                }
+            }
+        };
+    }
+
+    /**
+     * Loop over all the chars in given {@link UnicodeBlock}s and return a {@link Set <String>}
+     * containing all the possible values as their {@link String} values.
+     *
+     * @param blocks
+     *        the {@link UnicodeBlock}s to loop over
+     *
+     * @return a {@link Set <String>} containing all the possible values as {@link String} values
+     */
+    private static Set<String> getAllStringsFromUnicodeBlocks(final UnicodeBlock... blocks) {
+        final Set<UnicodeBlock> blockSet = new HashSet<>(Arrays.asList(blocks));
+        final Set<String> strings = new HashSet<>();
+
+        for (int codePoint = 0; codePoint <= Character.MAX_CODE_POINT; codePoint++) {
+            if (blockSet.contains(UnicodeBlock.of(codePoint))) {
+                final int charCount = Character.charCount(codePoint);
+                final StringBuilder sb = new StringBuilder(charCount);
+                if (charCount == 1) {
+                    sb.append(String.valueOf((char) codePoint));
+                } else if (charCount == 2) {
+                    sb.append(Character.highSurrogate(codePoint));
+                    sb.append(Character.lowSurrogate(codePoint));
+                } else {
+                    throw new IllegalArgumentException("Character.charCount of " + charCount + " not supported.");
+                }
+                strings.add(sb.toString());
+            }
+        }
+        return strings;
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFS() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeString(buffer, encoderState, "skipMe");
+        }
+
+        String expected = "expected-string-value";
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(String.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.STR8 & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(String.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.STR8 & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof String);
+
+        String value = (String) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testDecodeNonStringWhenStringExpectedReportsUsefulError() {
+        testDecodeNonStringWhenStringExpectedReportsUsefulError(false);
+    }
+
+    @Test
+    public void testDecodeNonStringWhenStringExpectedReportsUsefulErrorFS() {
+        testDecodeNonStringWhenStringExpectedReportsUsefulError(true);
+    }
+
+    private void testDecodeNonStringWhenStringExpectedReportsUsefulError(boolean fromStream) {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final UUID encoded = UUID.randomUUID();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(encoded.getMostSignificantBits());
+        buffer.writeLong(encoded.getLeastSignificantBits());
+
+        TypeDecoder<?> nextType = decoder.peekNextTypeDecoder(buffer, decoderState);
+        assertEquals(UUID.class, nextType.getTypeClass());
+
+        buffer.markReadIndex();
+
+        if (fromStream) {
+            try {
+                streamDecoder.readString(stream, streamDecoderState);
+            } catch (DecodeException ex) {
+                // Should indicate the type that it found in the error
+                assertTrue(ex.getMessage().contains(EncodingCodes.toString(EncodingCodes.UUID)));
+            }
+        } else {
+            try {
+                decoder.readString(buffer, decoderState);
+            } catch (DecodeException ex) {
+                // Should indicate the type that it found in the error
+                assertTrue(ex.getMessage().contains(EncodingCodes.toString(EncodingCodes.UUID)));
+            }
+        }
+
+        buffer.resetReadIndex();
+        UUID actual = decoder.readUUID(buffer, decoderState);
+        assertEquals(encoded, actual);
+    }
+
+    @Test
+    public void testDecodeUnknownTypeWhenStringExpectedReportsUsefulError() {
+        testDecodeUnknownTypeWhenStringExpectedReportsUsefulError(false);
+    }
+
+    @Test
+    public void testDecodeUnknownTypeWhenStringExpectedReportsUsefulErrorFS() {
+        testDecodeUnknownTypeWhenStringExpectedReportsUsefulError(true);
+    }
+
+    private void testDecodeUnknownTypeWhenStringExpectedReportsUsefulError(boolean fromStream) {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0x01);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readString(stream, streamDecoderState);
+            } catch (DecodeException ex) {
+                // Should indicate the type that it found in the error
+                assertTrue(ex.getMessage().contains("Unknown-Type:0x01"));
+            }
+        } else {
+            try {
+                decoder.readString(buffer, decoderState);
+            } catch (DecodeException ex) {
+                // Should indicate the type that it found in the error
+                assertTrue(ex.getMessage().contains("Unknown-Type:0x01"));
+            }
+        }
+    }
+
+    @Test
+    public void testStreamSkipOfStringEncodingHandlesIOException() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, "test-string-value");
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertEquals(String.class, typeDecoder.getTypeClass());
+
+        stream = Mockito.spy(stream);
+
+        Mockito.when(stream.skip(Mockito.anyLong())).thenThrow(EOFException.class);
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+            fail("Expected an exception on skip of encoded string failure.");
+        } catch (DecodeException dex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/SymbolTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/SymbolTypeCodecTest.java
new file mode 100644
index 0000000..b48559d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/SymbolTypeCodecTest.java
@@ -0,0 +1,572 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class SymbolTypeCodecTest extends CodecTestSupport {
+
+    private final String SMALL_SYMBOL_VALUIE = "Small String";
+    private final String LARGE_SYMBOL_VALUIE = "Large String: " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog.";
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        if (fromStream) {
+            buffer.writeByte(EncodingCodes.UINT);
+            buffer.writeByte(EncodingCodes.UINT);
+
+            try {
+                streamDecoder.readSymbol(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readSymbol(stream, streamDecoderState, "");
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            buffer.writeByte(EncodingCodes.UINT);
+            buffer.writeByte(EncodingCodes.UINT);
+
+            try {
+                decoder.readSymbol(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readSymbol(buffer, decoderState, "");
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        testReadFromNullEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromNullEncodingCodeFS() throws IOException {
+        testReadFromNullEncodingCode(true);
+    }
+
+    private void testReadFromNullEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertNull(streamDecoder.readSymbol(stream, streamDecoderState));
+            assertEquals("", streamDecoder.readSymbol(stream, streamDecoderState, ""));
+        } else {
+            assertNull(decoder.readSymbol(buffer, decoderState));
+            assertEquals("", decoder.readSymbol(buffer, decoderState, ""));
+        }
+    }
+
+    @Test
+    public void testEncodeSmallSymbol() throws IOException {
+        doTestEncodeDecode(Symbol.valueOf(SMALL_SYMBOL_VALUIE), false);
+    }
+
+    @Test
+    public void testEncodeLargeSymbol() throws IOException {
+        doTestEncodeDecode(Symbol.valueOf(LARGE_SYMBOL_VALUIE), false);
+    }
+
+    @Test
+    public void testEncodeEmptySymbol() throws IOException {
+        doTestEncodeDecode(Symbol.valueOf(""), false);
+    }
+
+    @Test
+    public void testEncodeNullSymbol() throws IOException {
+        doTestEncodeDecode(null, false);
+    }
+
+    @Test
+    public void testEncodeSmallSymbolFS() throws IOException {
+        doTestEncodeDecode(Symbol.valueOf(SMALL_SYMBOL_VALUIE), true);
+    }
+
+    @Test
+    public void testEncodeLargeSymbolFS() throws IOException {
+        doTestEncodeDecode(Symbol.valueOf(LARGE_SYMBOL_VALUIE), true);
+    }
+
+    @Test
+    public void testEncodeEmptySymbolFS() throws IOException {
+        doTestEncodeDecode(Symbol.valueOf(""), true);
+    }
+
+    @Test
+    public void testEncodeNullSymbolFS() throws IOException {
+        doTestEncodeDecode(null, true);
+    }
+
+    private void doTestEncodeDecode(Symbol value, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeSymbol(buffer, encoderState, value);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readSymbol(stream, streamDecoderState);
+        } else {
+            result = decoder.readSymbol(buffer, decoderState);
+        }
+
+        if (value != null) {
+            assertNotNull(result);
+            assertTrue(result instanceof Symbol);
+        } else {
+            assertNull(result);
+        }
+
+        assertEquals(value, result);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfSymbols() throws IOException {
+        doTestDecodeSymbolSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfSymbols() throws IOException {
+        doTestDecodeSymbolSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfSymbolsFS() throws IOException {
+        doTestDecodeSymbolSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfSymbolsFS() throws IOException {
+        doTestDecodeSymbolSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeSymbolSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeSymbol(buffer, encoderState, Symbol.valueOf(LARGE_SYMBOL_VALUIE));
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof Symbol);
+            assertEquals(LARGE_SYMBOL_VALUIE, result.toString());
+        }
+    }
+
+    @Test
+    public void testDecodeSmallSymbolArray() throws IOException {
+        doTestDecodeSymbolArrayType(SMALL_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSymbolArray() throws IOException {
+        doTestDecodeSymbolArrayType(LARGE_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSymbolArrayFS() throws IOException {
+        doTestDecodeSymbolArrayType(SMALL_ARRAY_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSymbolArrayFS() throws IOException {
+        doTestDecodeSymbolArrayType(LARGE_ARRAY_SIZE, true);
+    }
+
+    private void doTestDecodeSymbolArrayType(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Symbol[] source = new Symbol[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = Symbol.valueOf("test->" + i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Symbol[] array = (Symbol[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testEmptyShortSymbolEncode() throws IOException {
+        doTestEmptySymbolEncodeAsGivenType(EncodingCodes.SYM8, false);
+    }
+
+    @Test
+    public void testEmptyLargeSymbolEncode() throws IOException {
+        doTestEmptySymbolEncodeAsGivenType(EncodingCodes.SYM32, false);
+    }
+
+    @Test
+    public void testEmptyShortSymbolEncodeFS() throws IOException {
+        doTestEmptySymbolEncodeAsGivenType(EncodingCodes.SYM8, true);
+    }
+
+    @Test
+    public void testEmptyLargeSymbolEncodeFS() throws IOException {
+        doTestEmptySymbolEncodeAsGivenType(EncodingCodes.SYM32, true);
+    }
+
+    public void doTestEmptySymbolEncodeAsGivenType(byte encodingCode, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(encodingCode);
+        buffer.writeInt(0);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.peekNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Symbol.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), encodingCode & 0xFF);
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.peekNextTypeDecoder(buffer, decoderState);
+            assertEquals(Symbol.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), encodingCode & 0xFF);
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readSymbol(stream, streamDecoderState);
+        } else {
+            result = decoder.readSymbol(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertEquals("", result.toString());
+    }
+
+    @Test
+    public void testEmptyShortSymbolEncodeAsString() throws IOException {
+        doTestEmptySymbolEncodeAsGivenTypeReadAsString(EncodingCodes.SYM8, false);
+    }
+
+    @Test
+    public void testEmptyLargeSymbolEncodeAsString() throws IOException {
+        doTestEmptySymbolEncodeAsGivenTypeReadAsString(EncodingCodes.SYM32, false);
+    }
+
+    @Test
+    public void testEmptyShortSymbolEncodeAsStringFS() throws IOException {
+        doTestEmptySymbolEncodeAsGivenTypeReadAsString(EncodingCodes.SYM8, true);
+    }
+
+    @Test
+    public void testEmptyLargeSymbolEncodeAsStringFS() throws IOException {
+        doTestEmptySymbolEncodeAsGivenTypeReadAsString(EncodingCodes.SYM32, true);
+    }
+
+    public void doTestEmptySymbolEncodeAsGivenTypeReadAsString(byte encodingCode, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(encodingCode);
+        buffer.writeInt(0);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readSymbol(stream, streamDecoderState, "");
+        } else {
+            result = decoder.readSymbol(buffer, decoderState, "");
+        }
+
+        assertNotNull(result);
+        assertEquals("", result);
+    }
+
+    @Test
+    public void testEncodedSizeExceedsRemainingDetectedSym32() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.SYM32);
+        buffer.writeInt(Integer.MAX_VALUE);
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testEncodedSizeExceedsRemainingDetectedSym8() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        buffer.writeByte(EncodingCodes.SYM8);
+        buffer.writeByte(Byte.MAX_VALUE);
+
+        try {
+            decoder.readObject(buffer, decoderState);
+            fail("should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSymbolArray50() throws Throwable {
+        doEncodeDecodeSmallSymbolArrayTestImpl(50, false);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSymbolArray100() throws Throwable {
+        doEncodeDecodeSmallSymbolArrayTestImpl(100, false);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSymbolArray384() throws Throwable {
+        doEncodeDecodeSmallSymbolArrayTestImpl(384, false);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSymbolArray50FS() throws Throwable {
+        doEncodeDecodeSmallSymbolArrayTestImpl(50, true);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSymbolArray100FS() throws Throwable {
+        doEncodeDecodeSmallSymbolArrayTestImpl(100, true);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSymbolArray384FS() throws Throwable {
+        doEncodeDecodeSmallSymbolArrayTestImpl(384, true);
+    }
+
+    private void doEncodeDecodeSmallSymbolArrayTestImpl(int count, boolean fromStream) throws Throwable {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Symbol[] source = createPayloadArraySmallSymbols(count);
+
+        try {
+            assertEquals(count, source.length, "Unexpected source array length");
+
+            int encodingWidth = 4;
+            int arrayPayloadSize = encodingWidth + 1 + (count * 5); // variable width for element count + byte type descriptor + (number of elements * size[=length+content-char])
+            int expectedEncodedArraySize = 1 + encodingWidth + arrayPayloadSize; // array type code + variable width for array size + other encoded payload
+            byte[] expectedEncoding = new byte[expectedEncodedArraySize];
+            ProtonBuffer expectedEncodingWrapper = ProtonByteBufferAllocator.DEFAULT.wrap(expectedEncoding);
+            expectedEncodingWrapper.setWriteIndex(0);
+
+            // Write the array encoding code, array size, and element count
+            expectedEncodingWrapper.writeByte((byte) 0xF0); // 'array32' type descriptor code
+            expectedEncodingWrapper.writeInt(arrayPayloadSize);
+            expectedEncodingWrapper.writeInt(count);
+
+            // Write the type descriptor
+            expectedEncodingWrapper.writeByte((byte) 0xb3); // 'sym32' type descriptor code
+
+            // Write the elements
+            for (int i = 0; i < count; i++) {
+                Symbol symbol = source[i];
+                assertEquals(1, symbol.getLength(), "Unexpected length");
+
+                expectedEncodingWrapper.writeInt(1); // Length
+                expectedEncodingWrapper.writeByte(symbol.toString().charAt(0)); // Content
+            }
+
+            assertFalse(expectedEncodingWrapper.isWritable(), "Should have filled expected encoding array");
+
+            // Now verify against the actual encoding of the array
+            assertEquals(0, buffer.getReadIndex(), "Unexpected buffer position");
+            encoder.writeArray(buffer, encoderState, source);
+            assertEquals(expectedEncodedArraySize, buffer.getReadableBytes(), "Unexpected encoded payload length");
+
+            byte[] actualEncoding = new byte[expectedEncodedArraySize];
+            buffer.markReadIndex();
+            buffer.readBytes(actualEncoding);
+            assertFalse(buffer.isReadable(), "Should have drained the encoder buffer contents");
+
+            assertArrayEquals(expectedEncoding, actualEncoding, "Unexpected actual array encoding");
+
+            // Now verify against the decoding
+            buffer.resetReadIndex();
+
+            final Object decoded;
+            if (fromStream) {
+                decoded = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoded = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(decoded);
+            assertTrue(decoded.getClass().isArray());
+            assertEquals(Symbol.class, decoded.getClass().getComponentType());
+
+            assertArrayEquals(source, (Symbol[]) decoded, "Unexpected decoding");
+        } catch (Throwable t) {
+            System.err.println("Error during test, source array: " + Arrays.toString(source));
+            throw t;
+        }
+    }
+
+    // Creates 1 char Symbols with chars of 0-9, for encoding as sym8
+    private static Symbol[] createPayloadArraySmallSymbols(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        Symbol[] payload = new Symbol[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = Symbol.valueOf(String.valueOf(rand.nextInt(9)));
+        }
+
+        return payload;
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeSymbol(buffer, encoderState, Symbol.valueOf("skipMe"));
+        }
+
+        Symbol expected = Symbol.valueOf("expected-symbol-value");
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Symbol.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Symbol.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Symbol);
+
+        Symbol value = (Symbol) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testStreamSkipOfEncodingHandlesIOException() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeObject(buffer, encoderState, Symbol.valueOf("TEST-SYMBOL-VALUE"));
+
+        StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+        assertEquals(Symbol.class, typeDecoder.getTypeClass());
+
+        stream = Mockito.spy(stream);
+
+        Mockito.when(stream.skip(Mockito.anyLong())).thenThrow(EOFException.class);
+
+        try {
+            typeDecoder.skipValue(stream, streamDecoderState);
+            fail("Expected an exception on skip of encoded list failure.");
+        } catch (DecodeException dex) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/TimestampTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/TimestampTypeCodecTest.java
new file mode 100644
index 0000000..8b4c5cb
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/TimestampTypeCodecTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.primitives.TimestampTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.primitives.TimestampTypeEncoder;
+import org.junit.jupiter.api.Test;
+
+public class TimestampTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readTimestamp(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readTimestamp(stream, streamDecoderState, 42l);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readTimestamp(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readTimestamp(buffer, decoderState, 42l);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        testReadFromNullEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromNullEncodingCodeFS() throws IOException {
+        testReadFromNullEncodingCode(true);
+    }
+
+    private void testReadFromNullEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertNull(streamDecoder.readTimestamp(stream, streamDecoderState));
+            assertEquals(42l, streamDecoder.readTimestamp(stream, streamDecoderState, 42l));
+        } else {
+            assertNull(decoder.readTimestamp(buffer, decoderState));
+            assertEquals(42l, decoder.readTimestamp(buffer, decoderState, 42l));
+        }
+    }
+
+    @Test
+    public void testGetTypeCode() {
+        assertEquals(EncodingCodes.TIMESTAMP, (byte) new TimestampTypeDecoder().getTypeCode());
+    }
+
+    @Test
+    public void testGetTypeClass() {
+        assertEquals(Date.class, new TimestampTypeEncoder().getTypeClass());
+        assertEquals(Long.class, new TimestampTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testReadFromEncodingCode() throws IOException {
+        testReadFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromEncodingCodeFS() throws IOException {
+        testReadFromEncodingCode(true);
+    }
+
+    private void testReadFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.TIMESTAMP);
+        buffer.writeLong(42);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readTimestamp(stream, streamDecoderState).longValue());
+        } else {
+            assertEquals(42, decoder.readTimestamp(buffer, decoderState).longValue());
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeTimestamp(buffer, encoderState, Long.MAX_VALUE);
+            encoder.writeTimestamp(buffer, encoderState, 16);
+        }
+
+        long expected = 42;
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.TIMESTAMP & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.TIMESTAMP & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.TIMESTAMP & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Long.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.TIMESTAMP & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Long);
+
+        Long value = (Long) result;
+        assertEquals(expected, value.shortValue());
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int size = 10;
+
+        Date[] source = new Date[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = new Date(random.nextLong());
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        long[] array = (long[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i].getTime(), array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Date[] source = new Date[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertTrue(result.getClass().getComponentType().isPrimitive());
+
+        long[] array = (long[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UUIDTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UUIDTypeCodecTest.java
new file mode 100644
index 0000000..4d52688
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UUIDTypeCodecTest.java
@@ -0,0 +1,473 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.junit.jupiter.api.Test;
+
+public class UUIDTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFromStream() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readUUID(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readUUID(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadFromNullEncodingCode() throws IOException {
+        testReadFromNullEncodingCode(false);
+    }
+
+    @Test
+    public void testReadFromNullEncodingCodeFromStream() throws IOException {
+        testReadFromNullEncodingCode(true);
+    }
+
+    private void testReadFromNullEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final UUID value = UUID.randomUUID();
+
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.UUID);
+        buffer.writeLong(value.getMostSignificantBits());
+        buffer.writeLong(value.getLeastSignificantBits());
+
+        if (fromStream) {
+            assertNull(streamDecoder.readUUID(stream, streamDecoderState));
+            assertEquals(value, streamDecoder.readUUID(stream, streamDecoderState));
+        } else {
+            assertNull(decoder.readUUID(buffer, decoderState));
+            assertEquals(value, decoder.readUUID(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeUUID() throws IOException {
+        doTestEncodeDecodeUUIDSeries(1, false);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSeriesOfUUIDs() throws IOException {
+        doTestEncodeDecodeUUIDSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testEncodeDecodeLargeSeriesOfUUIDs() throws IOException {
+        doTestEncodeDecodeUUIDSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testEncodeDecodeUUIDFromStream() throws IOException {
+        doTestEncodeDecodeUUIDSeries(1, true);
+    }
+
+    @Test
+    public void testEncodeDecodeSmallSeriesOfUUIDsFromStream() throws IOException {
+        doTestEncodeDecodeUUIDSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testEncodeDecodeLargeSeriesOfUUIDsFromStream() throws IOException {
+        doTestEncodeDecodeUUIDSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestEncodeDecodeUUIDSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UUID[] source = new UUID[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UUID.randomUUID();
+        }
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeObject(buffer, encoderState, source[i]);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final Object result;
+            if (fromStream) {
+                result = streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                result = decoder.readObject(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertTrue(result instanceof UUID);
+
+            UUID decoded = (UUID) result;
+
+            assertEquals(source[i], decoded);
+        }
+    }
+
+    @Test
+    public void testDecodeSmallUUIDArray() throws IOException {
+        doTestDecodeUUDIArrayType(SMALL_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeUUDIArray() throws IOException {
+        doTestDecodeUUDIArrayType(LARGE_ARRAY_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallUUIDArrayFromStream() throws IOException {
+        doTestDecodeUUDIArrayType(SMALL_ARRAY_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeUUDIArrayFromStream() throws IOException {
+        doTestDecodeUUDIArrayType(LARGE_ARRAY_SIZE, true);
+    }
+
+    private void doTestDecodeUUDIArrayType(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UUID[] source = new UUID[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UUID.randomUUID();
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        UUID[] array = (UUID[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testWriteUUIDArrayWithMixedNullAndNotNullValues() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        UUID[] source = new UUID[2];
+        source[0] = UUID.randomUUID();
+        source[1] = null;
+
+        try {
+            encoder.writeArray(buffer, encoderState, source);
+            fail("Should not be able to encode array with mixed null and non-null values");
+        } catch (Exception e) {}
+
+        source = new UUID[2];
+        source[0] = null;
+        source[1] = UUID.randomUUID();
+
+        try {
+            encoder.writeArray(buffer, encoderState, source);
+            fail("Should not be able to encode array with mixed null and non-null values");
+        } catch (Exception e) {}
+    }
+
+    @Test
+    public void testWriteUUIDArrayWithZeroSize() throws IOException {
+        testWriteUUIDArrayWithZeroSize(false);
+    }
+
+    @Test
+    public void testWriteUUIDArrayWithZeroSizeFromStream() throws IOException {
+        testWriteUUIDArrayWithZeroSize(true);
+    }
+
+    private void testWriteUUIDArrayWithZeroSize(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UUID[] source = new UUID[0];
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        UUID[] array = (UUID[]) result;
+        assertEquals(0, array.length);
+    }
+
+    @Test
+    public void testObjectArrayContainingUUID() throws IOException {
+        testObjectArrayContainingUUID(false);
+    }
+
+    @Test
+    public void testObjectArrayContainingUUIDFromStream() throws IOException {
+        testObjectArrayContainingUUID(true);
+    }
+
+    private void testObjectArrayContainingUUID(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Object[] source = new Object[10];
+        for (int i = 0; i < 10; ++i) {
+            source[i] = UUID.randomUUID();
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        UUID[] array = (UUID[]) result;
+        assertEquals(10, array.length);
+
+        for (int i = 0; i < 10; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testWriteArrayOfUUIDArrayWithZeroSize() throws IOException {
+        testWriteArrayOfUUIDArrayWithZeroSize(false);
+    }
+
+    @Test
+    public void testWriteArrayOfUUIDArrayWithZeroSizeFromStream() throws IOException {
+        testWriteArrayOfUUIDArrayWithZeroSize(true);
+    }
+
+    private void testWriteArrayOfUUIDArrayWithZeroSize(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UUID[][] source = new UUID[2][0];
+        try {
+            encoder.writeArray(buffer, encoderState, source);
+        } catch (Exception e) {
+            fail("Should be able to encode array with no size");
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+
+        Object[] resultArray = (Object[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            Object nested = resultArray[i];
+            assertNotNull(result);
+            assertTrue(nested.getClass().isArray());
+
+            UUID[] uuids = (UUID[]) nested;
+            assertEquals(0, uuids.length);
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeUUID(buffer, encoderState, UUID.randomUUID());
+        }
+
+        UUID expected = UUID.randomUUID();
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UUID & 0xFF);
+                assertEquals(UUID.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UUID & 0xFF);
+                assertEquals(UUID.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof UUID);
+
+        UUID value = (UUID) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFromStream() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        UUID[] source = new UUID[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UUID.randomUUID();
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UUID[] array = (UUID[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFromStream() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UUID[] source = new UUID[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UUID[] array = (UUID[]) result;
+        assertEquals(source.length, array.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedByteTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedByteTypeCodecTest.java
new file mode 100644
index 0000000..9383095
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedByteTypeCodecTest.java
@@ -0,0 +1,480 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.junit.jupiter.api.Test;
+
+public class UnsignedByteTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readUnsignedByte(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readUnsignedByte(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readUnsignedByte(buffer, decoderState, (byte) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadTypeFromEncodingCode() throws IOException {
+        testReadTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadTypeFromEncodingCodeFS() throws IOException {
+        testReadTypeFromEncodingCode(true);
+    }
+
+    public void testReadTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte((byte) 42);
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte((byte) 43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readUnsignedByte(stream, streamDecoderState).intValue());
+            assertEquals(43, streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 42));
+            assertNull(streamDecoder.readUnsignedByte(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 42));
+        } else {
+            assertEquals(42, decoder.readUnsignedByte(buffer, decoderState).intValue());
+            assertEquals(43, decoder.readUnsignedByte(buffer, decoderState, (byte) 42));
+            assertNull(decoder.readUnsignedByte(buffer, decoderState));
+            assertEquals(42, decoder.readUnsignedByte(buffer, decoderState, (byte) 42));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedByte() throws Exception {
+        testEncodeDecodeUnsignedByte(false);
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedByteFS() throws Exception {
+        testEncodeDecodeUnsignedByte(true);
+    }
+
+    public void testEncodeDecodeUnsignedByte(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedByte(buffer, encoderState, UnsignedByte.valueOf((byte) 64));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedByte);
+        assertEquals(64, ((UnsignedByte) result).byteValue());
+    }
+
+    @Test
+    public void testEncodeDecodeByte() throws Exception {
+        testEncodeDecodeByte(false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteFS() throws Exception {
+        testEncodeDecodeByte(true);
+    }
+
+    private void testEncodeDecodeByte(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedByte(buffer, encoderState, (byte) 64);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedByte);
+        assertEquals(64, ((UnsignedByte) result).byteValue());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedBytes() throws IOException {
+        doTestDecodeUnsignedByteSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedBytes() throws IOException {
+        doTestDecodeUnsignedByteSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedBytesFS() throws IOException {
+        doTestDecodeUnsignedByteSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedBytesFS() throws IOException {
+        doTestDecodeUnsignedByteSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeUnsignedByteSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeUnsignedByte(buffer, encoderState, (byte)(i % 255));
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final UnsignedByte result;
+            if (fromStream) {
+                result = streamDecoder.readUnsignedByte(stream, streamDecoderState);
+            } else {
+                result = decoder.readUnsignedByte(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertEquals((byte)(i % 255), result.byteValue());
+        }
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        UnsignedByte[] source = new UnsignedByte[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UnsignedByte.valueOf((byte) (i % 255));
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedByte[] array = (UnsignedByte[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UnsignedByte[] source = new UnsignedByte[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedByte[] array = (UnsignedByte[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testDecodeEncodedBytes() throws Exception {
+        testDecodeEncodedBytes(false);
+    }
+
+    @Test
+    public void testDecodeEncodedBytesFS() throws Exception {
+        testDecodeEncodedBytes(true);
+    }
+
+    private void testDecodeEncodedBytes(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(127);
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(255);
+
+        if (fromStream) {
+            UnsignedByte result1 = streamDecoder.readUnsignedByte(stream, streamDecoderState);
+            UnsignedByte result2 = streamDecoder.readUnsignedByte(stream, streamDecoderState);
+            UnsignedByte result3 = streamDecoder.readUnsignedByte(stream, streamDecoderState);
+
+            assertEquals(UnsignedByte.valueOf((byte) 0), result1);
+            assertEquals(UnsignedByte.valueOf((byte) 127), result2);
+            assertEquals(UnsignedByte.valueOf((byte) 255), result3);
+        } else {
+            UnsignedByte result1 = decoder.readUnsignedByte(buffer, decoderState);
+            UnsignedByte result2 = decoder.readUnsignedByte(buffer, decoderState);
+            UnsignedByte result3 = decoder.readUnsignedByte(buffer, decoderState);
+
+            assertEquals(UnsignedByte.valueOf((byte) 0), result1);
+            assertEquals(UnsignedByte.valueOf((byte) 127), result2);
+            assertEquals(UnsignedByte.valueOf((byte) 255), result3);
+        }
+    }
+
+    @Test
+    public void testDecodeEncodedBytesAsPrimitive() throws Exception {
+        testDecodeEncodedBytesAsPrimitive(false);
+    }
+
+    @Test
+    public void testDecodeEncodedBytesAsPrimitiveFS() throws Exception {
+        testDecodeEncodedBytesAsPrimitive(true);
+    }
+
+    private void testDecodeEncodedBytesAsPrimitive(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(0);
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(127);
+        buffer.writeByte(EncodingCodes.UBYTE);
+        buffer.writeByte(255);
+
+        if (fromStream) {
+            byte result1 = streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 1);
+            byte result2 = streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 105);
+            byte result3 = streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 200);
+
+            assertEquals((byte) 0, result1);
+            assertEquals((byte) 127, result2);
+            assertEquals((byte) 255, result3);
+        } else {
+            byte result1 = decoder.readUnsignedByte(buffer, decoderState, (byte) 1);
+            byte result2 = decoder.readUnsignedByte(buffer, decoderState, (byte) 105);
+            byte result3 = decoder.readUnsignedByte(buffer, decoderState, (byte) 200);
+
+            assertEquals((byte) 0, result1);
+            assertEquals((byte) 127, result2);
+            assertEquals((byte) 255, result3);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanFromNullEncoding() throws Exception {
+        testDecodeBooleanFromNullEncoding(false);
+    }
+
+    @Test
+    public void testDecodeBooleanFromNullEncodingFS() throws Exception {
+        testDecodeBooleanFromNullEncoding(true);
+    }
+
+    private void testDecodeBooleanFromNullEncoding(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedByte(buffer, encoderState, (byte) 1);
+        encoder.writeNull(buffer, encoderState);
+
+        if (fromStream) {
+            UnsignedByte result1 = streamDecoder.readUnsignedByte(stream, streamDecoderState);
+            UnsignedByte result2 = streamDecoder.readUnsignedByte(stream, streamDecoderState);
+
+            assertEquals(UnsignedByte.valueOf((byte) 1), result1);
+            assertNull(result2);
+        } else {
+            UnsignedByte result1 = decoder.readUnsignedByte(buffer, decoderState);
+            UnsignedByte result2 = decoder.readUnsignedByte(buffer, decoderState);
+
+            assertEquals(UnsignedByte.valueOf((byte) 1), result1);
+            assertNull(result2);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanAsPrimitiveWithDefault() throws Exception {
+        testDecodeBooleanAsPrimitiveWithDefault(false);
+    }
+
+    @Test
+    public void testDecodeBooleanAsPrimitiveWithDefaultFS() throws Exception {
+        testDecodeBooleanAsPrimitiveWithDefault(true);
+    }
+
+    public void testDecodeBooleanAsPrimitiveWithDefault(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedByte(buffer, encoderState, (byte) 27);
+        encoder.writeNull(buffer, encoderState);
+
+        if (fromStream) {
+            byte result = streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 0);
+            assertEquals((byte) 27, result);
+            result = streamDecoder.readUnsignedByte(stream, streamDecoderState, (byte) 127);
+            assertEquals((byte) 127, result);
+        } else {
+            byte result = decoder.readUnsignedByte(buffer, decoderState, (byte) 0);
+            assertEquals((byte) 27, result);
+            result = decoder.readUnsignedByte(buffer, decoderState, (byte) 127);
+            assertEquals((byte) 127, result);
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeUnsignedByte(buffer, encoderState, UnsignedByte.valueOf((byte) i));
+        }
+
+        UnsignedByte expected = UnsignedByte.valueOf((byte) 42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(UnsignedByte.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UBYTE & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(UnsignedByte.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UBYTE & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnsignedByte);
+
+        UnsignedByte value = (UnsignedByte) result;
+        assertEquals(expected, value);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedIntegerTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedIntegerTypeCodecTest.java
new file mode 100644
index 0000000..e948968
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedIntegerTypeCodecTest.java
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+public class UnsignedIntegerTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeByte(EncodingCodes.ULONG);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readUnsignedInteger(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readUnsignedInteger(stream, streamDecoderState, (long) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readUnsignedInteger(stream, streamDecoderState, 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readUnsignedInteger(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readUnsignedInteger(buffer, decoderState, (long) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readUnsignedInteger(buffer, decoderState, 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadTypeFromEncodingCode() throws IOException {
+        testReadTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadTypeFromEncodingCodeFS() throws IOException {
+        testReadTypeFromEncodingCode(true);
+    }
+
+    public void testReadTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT0);
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeInt(42);
+        buffer.writeByte(EncodingCodes.SMALLUINT);
+        buffer.writeByte((byte) 43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(0, streamDecoder.readUnsignedInteger(stream, streamDecoderState).intValue());
+            assertEquals(42, streamDecoder.readUnsignedInteger(stream, streamDecoderState).intValue());
+            assertEquals(43, streamDecoder.readUnsignedInteger(stream, streamDecoderState, 42));
+            assertNull(streamDecoder.readUnsignedInteger(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readUnsignedInteger(stream, streamDecoderState, 42));
+            assertEquals(42, streamDecoder.readUnsignedInteger(stream, streamDecoderState, (long) 42));
+        } else {
+            assertEquals(0, decoder.readUnsignedInteger(buffer, decoderState).intValue());
+            assertEquals(42, decoder.readUnsignedInteger(buffer, decoderState).intValue());
+            assertEquals(43, decoder.readUnsignedInteger(buffer, decoderState, 42));
+            assertNull(decoder.readUnsignedInteger(buffer, decoderState));
+            assertEquals(42, decoder.readUnsignedInteger(buffer, decoderState, 42));
+            assertEquals(42, decoder.readUnsignedInteger(buffer, decoderState, (long) 42));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedInteger() throws Exception {
+        testEncodeDecodeUnsignedInteger(false);
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedIntegerFS() throws Exception {
+        testEncodeDecodeUnsignedInteger(true);
+    }
+
+    private void testEncodeDecodeUnsignedInteger(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(640));
+
+        if (fromStream) {
+            Object result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(640, ((UnsignedInteger) result).intValue());
+        } else {
+            Object result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(640, ((UnsignedInteger) result).intValue());
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeInteger() throws Exception {
+        testEncodeDecodeInteger(false);
+    }
+
+    @Test
+    public void testEncodeDecodeIntegerFS() throws Exception {
+        testEncodeDecodeInteger(true);
+    }
+
+    private void testEncodeDecodeInteger(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedInteger(buffer, encoderState, 640);
+        encoder.writeUnsignedInteger(buffer, encoderState, 0);
+        encoder.writeUnsignedInteger(buffer, encoderState, 255);
+        encoder.writeUnsignedInteger(buffer, encoderState, 254);
+
+        if (fromStream) {
+            Object result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(640, ((UnsignedInteger) result).intValue());
+            result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(0, ((UnsignedInteger) result).intValue());
+            result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(255, ((UnsignedInteger) result).intValue());
+            result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(254, ((UnsignedInteger) result).intValue());
+        } else {
+            Object result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(640, ((UnsignedInteger) result).intValue());
+            result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(0, ((UnsignedInteger) result).intValue());
+            result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(255, ((UnsignedInteger) result).intValue());
+            result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(254, ((UnsignedInteger) result).intValue());
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeLong() throws Exception {
+        testEncodeDecodeLong(false);
+    }
+
+    @Test
+    public void testEncodeDecodeLongFS() throws Exception {
+        testEncodeDecodeLong(true);
+    }
+
+    private void testEncodeDecodeLong(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedInteger(buffer, encoderState, 640l);
+        encoder.writeUnsignedInteger(buffer, encoderState, 0l);
+
+        if (fromStream) {
+            Object result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(640, ((UnsignedInteger) result).intValue());
+            result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(0, ((UnsignedInteger) result).intValue());
+        } else {
+            Object result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(640, ((UnsignedInteger) result).intValue());
+            result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(0, ((UnsignedInteger) result).intValue());
+        }
+
+        try {
+            encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Should fail on value out of range");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testEncodeDecodeByte() throws Exception {
+        testEncodeDecodeByte(false);
+    }
+
+    @Test
+    public void testEncodeDecodeByteFS() throws Exception {
+        testEncodeDecodeByte(true);
+    }
+
+    private void testEncodeDecodeByte(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedInteger(buffer, encoderState, (byte) 64);
+        encoder.writeUnsignedInteger(buffer, encoderState, (byte) 0);
+
+        if (fromStream) {
+            Object result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(64, ((UnsignedInteger) result).byteValue());
+            result = streamDecoder.readObject(stream, streamDecoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(0, ((UnsignedInteger) result).byteValue());
+        } else {
+            Object result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(64, ((UnsignedInteger) result).byteValue());
+            result = decoder.readObject(buffer, decoderState);
+            assertTrue(result instanceof UnsignedInteger);
+            assertEquals(0, ((UnsignedInteger) result).byteValue());
+        }
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedIntegers() throws IOException {
+        doTestDecodeUnsignedIntegerSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedIntegers() throws IOException {
+        doTestDecodeUnsignedIntegerSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedIntegersFS() throws IOException {
+        doTestDecodeUnsignedIntegerSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedIntegersFS() throws IOException {
+        doTestDecodeUnsignedIntegerSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeUnsignedIntegerSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeUnsignedInteger(buffer, encoderState, i);
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final UnsignedInteger result;
+            if (fromStream) {
+                result = streamDecoder.readUnsignedInteger(stream, streamDecoderState);
+            } else {
+                result = decoder.readUnsignedInteger(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertEquals(i, result.intValue());
+        }
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        UnsignedInteger[] source = new UnsignedInteger[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UnsignedInteger.valueOf(i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedInteger[] array = (UnsignedInteger[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UnsignedInteger[] source = new UnsignedInteger[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedInteger[] array = (UnsignedInteger[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(0));
+
+        for (int i = 1; i <= 10; ++i) {
+            encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(Integer.MAX_VALUE - i));
+            encoder.writeUnsignedInteger(buffer, encoderState, UnsignedInteger.valueOf(i));
+        }
+
+        UnsignedInteger expected = UnsignedInteger.valueOf(42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(UnsignedInteger.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UINT0 & 0xFF);
+            typeDecoder.skipValue(stream, streamDecoderState);
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(UnsignedInteger.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UINT0 & 0xFF);
+            typeDecoder.skipValue(buffer, decoderState);
+        }
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(UnsignedInteger.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UINT & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(UnsignedInteger.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.SMALLUINT & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(UnsignedInteger.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.UINT & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(UnsignedInteger.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.SMALLUINT & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnsignedInteger);
+
+        UnsignedInteger value = (UnsignedInteger) result;
+        assertEquals(expected, value);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedLongTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedLongTypeCodecTest.java
new file mode 100644
index 0000000..d4ef457
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedLongTypeCodecTest.java
@@ -0,0 +1,682 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.junit.jupiter.api.Test;
+
+public class UnsignedLongTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readUnsignedLong(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readUnsignedLong(stream, streamDecoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readUnsignedLong(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readUnsignedLong(buffer, decoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testReadUByteFromEncodingCode() throws IOException {
+        testReadUByteFromEncodingCode(false);
+    }
+
+    @Test
+    public void testReadUByteFromEncodingCodeFS() throws IOException {
+        testReadUByteFromEncodingCode(true);
+    }
+
+    private void testReadUByteFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.ULONG0);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(42);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte((byte) 43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(0, streamDecoder.readUnsignedLong(stream, streamDecoderState).intValue());
+            assertEquals(42, streamDecoder.readUnsignedLong(stream, streamDecoderState).intValue());
+            assertEquals(43, streamDecoder.readUnsignedLong(stream, streamDecoderState, 42));
+            assertNull(streamDecoder.readUnsignedLong(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readUnsignedLong(stream, streamDecoderState, 42));
+        } else {
+            assertEquals(0, decoder.readUnsignedLong(buffer, decoderState).intValue());
+            assertEquals(42, decoder.readUnsignedLong(buffer, decoderState).intValue());
+            assertEquals(43, decoder.readUnsignedLong(buffer, decoderState, 42));
+            assertNull(decoder.readUnsignedLong(buffer, decoderState));
+            assertEquals(42, decoder.readUnsignedLong(buffer, decoderState, 42));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedLong() throws Exception {
+        testEncodeDecodeUnsignedLong(false);
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedLongFS() throws Exception {
+        testEncodeDecodeUnsignedLong(true);
+    }
+
+    private void testEncodeDecodeUnsignedLong(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(640));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedLong);
+        assertEquals(640, ((UnsignedLong) result).intValue());
+    }
+
+    @Test
+    public void testEncodeDecodePrimitive() throws Exception {
+        testEncodeDecodePrimitive(false);
+    }
+
+    @Test
+    public void testEncodeDecodePrimitiveFS() throws Exception {
+        testEncodeDecodePrimitive(true);
+    }
+
+    private void testEncodeDecodePrimitive(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedLong(buffer, encoderState, 640l);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedLong);
+        assertEquals(640, ((UnsignedLong) result).intValue());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedLongs() throws IOException {
+        doTestDecodeUnsignedLongSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedLongs() throws IOException {
+        doTestDecodeUnsignedLongSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedLongsFS() throws IOException {
+        doTestDecodeUnsignedLongSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedLongsFS() throws IOException {
+        doTestDecodeUnsignedLongSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeUnsignedLongSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(i));
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final UnsignedLong result;
+            if (fromStream) {
+                result = streamDecoder.readUnsignedLong(stream, streamDecoderState);
+            } else {
+                result = decoder.readUnsignedLong(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertEquals(i, result.intValue());
+        }
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        UnsignedLong[] source = new UnsignedLong[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UnsignedLong.valueOf(i);
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedLong[] array = (UnsignedLong[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UnsignedLong[] source = new UnsignedLong[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedLong[] array = (UnsignedLong[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testDecodeEncodedBytes() throws Exception {
+        testDecodeEncodedBytes(false);
+    }
+
+    @Test
+    public void testDecodeEncodedBytesFS() throws Exception {
+        testDecodeEncodedBytes(true);
+    }
+
+    private void testDecodeEncodedBytes(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.ULONG0);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(127);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(255);
+
+        if (fromStream) {
+            UnsignedLong result1 = streamDecoder.readUnsignedLong(stream, streamDecoderState);
+            UnsignedLong result2 = streamDecoder.readUnsignedLong(stream, streamDecoderState);
+            UnsignedLong result3 = streamDecoder.readUnsignedLong(stream, streamDecoderState);
+
+            assertEquals(UnsignedLong.valueOf(0), result1);
+            assertEquals(UnsignedLong.valueOf(127), result2);
+            assertEquals(UnsignedLong.valueOf(255), result3);
+        } else {
+            UnsignedLong result1 = decoder.readUnsignedLong(buffer, decoderState);
+            UnsignedLong result2 = decoder.readUnsignedLong(buffer, decoderState);
+            UnsignedLong result3 = decoder.readUnsignedLong(buffer, decoderState);
+
+            assertEquals(UnsignedLong.valueOf(0), result1);
+            assertEquals(UnsignedLong.valueOf(127), result2);
+            assertEquals(UnsignedLong.valueOf(255), result3);
+        }
+    }
+
+    @Test
+    public void testDecodeEncodedBytesAsPrimitive() throws Exception {
+        testDecodeEncodedBytesAsPrimitive(false);
+    }
+
+    @Test
+    public void testDecodeEncodedBytesAsPrimitiveFS() throws Exception {
+        testDecodeEncodedBytesAsPrimitive(true);
+    }
+
+    private void testDecodeEncodedBytesAsPrimitive(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.ULONG0);
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(127);
+        buffer.writeByte(EncodingCodes.ULONG);
+        buffer.writeLong(255);
+
+        if (fromStream) {
+            long result1 = streamDecoder.readUnsignedLong(stream, streamDecoderState, 1);
+            long result2 = streamDecoder.readUnsignedLong(stream, streamDecoderState, 105);
+            long result3 = streamDecoder.readUnsignedLong(stream, streamDecoderState, 200);
+
+            assertEquals(0, result1);
+            assertEquals(127, result2);
+            assertEquals(255, result3);
+        } else {
+            long result1 = decoder.readUnsignedLong(buffer, decoderState, 1);
+            long result2 = decoder.readUnsignedLong(buffer, decoderState, 105);
+            long result3 = decoder.readUnsignedLong(buffer, decoderState, 200);
+
+            assertEquals(0, result1);
+            assertEquals(127, result2);
+            assertEquals(255, result3);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanFromNullEncoding() throws Exception {
+        testDecodeBooleanFromNullEncoding(false);
+    }
+
+    @Test
+    public void testDecodeBooleanFromNullEncodingFS() throws Exception {
+        testDecodeBooleanFromNullEncoding(true);
+    }
+
+    private void testDecodeBooleanFromNullEncoding(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedLong(buffer, encoderState, (byte) 1);
+        encoder.writeNull(buffer, encoderState);
+
+        if (fromStream) {
+            UnsignedLong result1 = streamDecoder.readUnsignedLong(stream, streamDecoderState);
+            UnsignedLong result2 = streamDecoder.readUnsignedLong(stream, streamDecoderState);
+
+            assertEquals(UnsignedLong.valueOf(1), result1);
+            assertNull(result2);
+        } else {
+            UnsignedLong result1 = decoder.readUnsignedLong(buffer, decoderState);
+            UnsignedLong result2 = decoder.readUnsignedLong(buffer, decoderState);
+
+            assertEquals(UnsignedLong.valueOf(1), result1);
+            assertNull(result2);
+        }
+    }
+
+    @Test
+    public void testDecodeBooleanAsPrimitiveWithDefault() throws Exception {
+        testDecodeBooleanAsPrimitiveWithDefault(false);
+    }
+
+    @Test
+    public void testDecodeBooleanAsPrimitiveWithDefaultFS() throws Exception {
+        testDecodeBooleanAsPrimitiveWithDefault(true);
+    }
+
+    private void testDecodeBooleanAsPrimitiveWithDefault(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedLong(buffer, encoderState, 27);
+        encoder.writeNull(buffer, encoderState);
+
+        if (fromStream) {
+            long result = streamDecoder.readUnsignedLong(stream, streamDecoderState, 0);
+            assertEquals(27, result);
+            result = streamDecoder.readUnsignedLong(stream, streamDecoderState, 127);
+            assertEquals(127, result);
+        } else {
+            long result = decoder.readUnsignedLong(buffer, decoderState, 0);
+            assertEquals(27, result);
+            result = decoder.readUnsignedLong(buffer, decoderState, 127);
+            assertEquals(127, result);
+        }
+    }
+
+    @Test
+    public void testWriteLongZeroEncodesAsOneByte() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, 0l);
+        assertEquals(1, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.ULONG0, buffer.readByte());
+    }
+
+    @Test
+    public void testWriteLongValuesInSmallULongRange() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, 1l);
+        assertEquals(2, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.SMALLULONG, buffer.readByte());
+        assertEquals((byte) 1, buffer.readByte());
+        encoder.writeUnsignedLong(buffer, encoderState, 64l);
+        assertEquals(2, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.SMALLULONG, buffer.readByte());
+        assertEquals((byte) 64, buffer.readByte());
+        encoder.writeUnsignedLong(buffer, encoderState, 255l);
+        assertEquals(2, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.SMALLULONG, buffer.readByte());
+        assertEquals((byte) 255, buffer.readByte());
+    }
+
+    @Test
+    public void testWriteLongValuesOutsideOfSmallULongRange() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, 314l);
+        assertEquals(9, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.ULONG, buffer.readByte());
+        assertEquals(314l, buffer.readLong());
+    }
+
+    @Test
+    public void testWriteByteZeroEncodesAsOneByte() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, (byte) 0);
+        assertEquals(1, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.ULONG0, buffer.readByte());
+    }
+
+    @Test
+    public void testWriteByteInSmallULongRange() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, (byte) 64);
+        assertEquals(2, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.SMALLULONG, buffer.readByte());
+        assertEquals((byte) 64, buffer.readByte());
+    }
+
+    @Test
+    public void testWriteByteAsZeroULong() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        encoder.writeUnsignedLong(buffer, encoderState, (byte) 0);
+        assertEquals(1, buffer.getReadableBytes());
+        assertEquals(EncodingCodes.ULONG0, buffer.readByte());
+        assertFalse(buffer.isReadable());
+    }
+
+    @Test
+    public void testReadULongZeroDoesNotTouchBuffer() throws IOException {
+        testReadULongZeroDoesNotTouchBuffer(false);
+    }
+
+    @Test
+    public void testReadULongZeroDoesNotTouchBufferFS() throws IOException {
+        testReadULongZeroDoesNotTouchBuffer(true);
+    }
+
+    private void testReadULongZeroDoesNotTouchBuffer(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(1, 1);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.ULONG0);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+            assertFalse(stream.available() > 0);
+            assertEquals(UnsignedLong.ZERO, typeDecoder.readValue(stream, streamDecoderState));
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+            assertFalse(buffer.isReadable());
+            assertEquals(UnsignedLong.ZERO, typeDecoder.readValue(buffer, decoderState));
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(0));
+
+        for (int i = 1; i <= 10; ++i) {
+            encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(Long.MAX_VALUE - i));
+            encoder.writeUnsignedLong(buffer, encoderState, UnsignedLong.valueOf(i));
+        }
+
+        UnsignedLong expected = UnsignedLong.valueOf(42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.ULONG0 & 0xFF);
+            typeDecoder.skipValue(stream, streamDecoderState);
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+            assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+            assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.ULONG0 & 0xFF);
+            typeDecoder.skipValue(buffer, decoderState);
+        }
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.ULONG & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+                typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.SMALLULONG & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.ULONG & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+                typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(UnsignedLong.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.SMALLULONG & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnsignedLong);
+
+        UnsignedLong value = (UnsignedLong) result;
+        assertEquals(expected, value);
+    }
+
+    @Test
+    public void testReadULongArray() throws IOException {
+        doTestReadULongArray(EncodingCodes.ULONG, false);
+    }
+
+    @Test
+    public void testReadULongArrayFromStream() throws IOException {
+        doTestReadULongArray(EncodingCodes.ULONG, true);
+    }
+
+    @Test
+    public void testReadSmallULongArray() throws IOException {
+        doTestReadULongArray(EncodingCodes.SMALLULONG, false);
+    }
+
+    @Test
+    public void testReadSmallULongArrayFromStream() throws IOException {
+        doTestReadULongArray(EncodingCodes.SMALLULONG, true);
+    }
+
+    @Test
+    public void testReadULong0Array() throws IOException {
+        doTestReadULongArray(EncodingCodes.ULONG0, false);
+    }
+
+    @Test
+    public void testReadULong0ArrayFromStream() throws IOException {
+        doTestReadULongArray(EncodingCodes.ULONG0, true);
+    }
+
+    public void doTestReadULongArray(byte encoding, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        if (encoding == EncodingCodes.ULONG) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(25);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.ULONG);
+            buffer.writeLong(1l);   // [0]
+            buffer.writeLong(2l);   // [1]
+        } else if (encoding == EncodingCodes.SMALLULONG) {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(11);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.SMALLULONG);
+            buffer.writeByte(1);   // [0]
+            buffer.writeByte(2);   // [1]
+        } else {
+            buffer.writeByte(EncodingCodes.ARRAY32);
+            buffer.writeInt(9);  // Size
+            buffer.writeInt(2);   // Count
+            buffer.writeByte(EncodingCodes.ULONG0);
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedLong[] array = (UnsignedLong[]) result;
+
+        assertEquals(2, array.length);
+
+        if (encoding == EncodingCodes.ULONG0) {
+            assertEquals(UnsignedLong.ZERO, array[0]);
+            assertEquals(UnsignedLong.ZERO, array[1]);
+        } else {
+            assertEquals(UnsignedLong.valueOf(1), array[0]);
+            assertEquals(UnsignedLong.valueOf(2), array[1]);
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedShortTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedShortTypeCodecTest.java
new file mode 100644
index 0000000..a28ab04
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/primitives/UnsignedShortTypeCodecTest.java
@@ -0,0 +1,414 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.primitives;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.PrimitiveTypeDecoder;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.junit.jupiter.api.Test;
+
+public class UnsignedShortTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(false);
+    }
+
+    @Test
+    public void testDecoderThrowsWhenAskedToReadWrongTypeAsThisTypeFS() throws Exception {
+        testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(true);
+    }
+
+    private void testDecoderThrowsWhenAskedToReadWrongTypeAsThisType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+        buffer.writeByte(EncodingCodes.UINT);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readUnsignedShort(stream, streamDecoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readUnsignedShort(stream, streamDecoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                streamDecoder.readUnsignedShort(stream, streamDecoderState, 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        } else {
+            try {
+                decoder.readUnsignedShort(buffer, decoderState);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readUnsignedShort(buffer, decoderState, (short) 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+
+            try {
+                decoder.readUnsignedShort(buffer, decoderState, 0);
+                fail("Should not allow read of integer type as this type");
+            } catch (DecodeException e) {}
+        }
+    }
+
+    @Test
+    public void testTypeFromEncodingCode() throws IOException {
+        testTypeFromEncodingCode(false);
+    }
+
+    @Test
+    public void testTypeFromEncodingCodeFS() throws IOException {
+        testTypeFromEncodingCode(true);
+    }
+
+    public void testTypeFromEncodingCode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte(EncodingCodes.USHORT);
+        buffer.writeShort((short) 42);
+        buffer.writeByte(EncodingCodes.USHORT);
+        buffer.writeShort((short) 43);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+        buffer.writeByte(EncodingCodes.NULL);
+
+        if (fromStream) {
+            assertEquals(42, streamDecoder.readUnsignedShort(stream, streamDecoderState).shortValue());
+            assertEquals(43, streamDecoder.readUnsignedShort(stream, streamDecoderState, (short) 42));
+            assertNull(streamDecoder.readUnsignedShort(stream, streamDecoderState));
+            assertEquals(42, streamDecoder.readUnsignedShort(stream, streamDecoderState, (short) 42));
+            assertEquals(43, streamDecoder.readUnsignedShort(stream, streamDecoderState, 43));
+        } else {
+            assertEquals(42, decoder.readUnsignedShort(buffer, decoderState).shortValue());
+            assertEquals(43, decoder.readUnsignedShort(buffer, decoderState, (short) 42));
+            assertNull(decoder.readUnsignedShort(buffer, decoderState));
+            assertEquals(42, decoder.readUnsignedShort(buffer, decoderState, (short) 42));
+            assertEquals(43, decoder.readUnsignedShort(buffer, decoderState, 43));
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedShort() throws Exception {
+        testEncodeDecodeUnsignedShort(false);
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedShortFS() throws Exception {
+        testEncodeDecodeUnsignedShort(true);
+    }
+
+    public void testEncodeDecodeUnsignedShort(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedShort(buffer, encoderState, UnsignedShort.valueOf((byte) 64));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedShort);
+        assertEquals(64, ((UnsignedShort) result).byteValue());
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedShortAbove32k() throws Exception {
+        testEncodeDecodeUnsignedShortAbove32k(false);
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedShortAbove32kFS() throws Exception {
+        testEncodeDecodeUnsignedShortAbove32k(true);
+    }
+
+    private void testEncodeDecodeUnsignedShortAbove32k(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedShort(buffer, encoderState, UnsignedShort.valueOf((short) 33565));
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedShort);
+        assertTrue(((UnsignedShort) result).shortValue() < 0);
+        assertEquals(33565, ((UnsignedShort) result).intValue());
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedShortFromInt() throws Exception {
+        testEncodeDecodeUnsignedShortFromInt(false);
+    }
+
+    @Test
+    public void testEncodeDecodeUnsignedShortFromIntFS() throws Exception {
+        testEncodeDecodeUnsignedShortFromInt(true);
+    }
+
+    private void testEncodeDecodeUnsignedShortFromInt(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedShort(buffer, encoderState, 33565);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedShort);
+        assertTrue(((UnsignedShort) result).shortValue() < 0);
+        assertEquals(33565, ((UnsignedShort) result).intValue());
+
+        try {
+            encoder.writeUnsignedShort(buffer, encoderState, 65536);
+            fail("Should not be able to write illegal out of range value");
+        } catch (IllegalArgumentException iae) {
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeShort() throws Exception {
+        testEncodeDecodeShort(false);
+    }
+
+    @Test
+    public void testEncodeDecodeShortFS() throws Exception {
+        testEncodeDecodeShort(true);
+    }
+
+    private void testEncodeDecodeShort(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        encoder.writeUnsignedShort(buffer, encoderState, (short) 64);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result instanceof UnsignedShort);
+        assertEquals(64, ((UnsignedShort) result).shortValue());
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedShorts() throws IOException {
+        doTestDecodeUnsignedShortSeries(SMALL_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedShorts() throws IOException {
+        doTestDecodeUnsignedShortSeries(LARGE_SIZE, false);
+    }
+
+    @Test
+    public void testDecodeSmallSeriesOfUnsignedShortsFS() throws IOException {
+        doTestDecodeUnsignedShortSeries(SMALL_SIZE, true);
+    }
+
+    @Test
+    public void testDecodeLargeSeriesOfUnsignedShortsFS() throws IOException {
+        doTestDecodeUnsignedShortSeries(LARGE_SIZE, true);
+    }
+
+    private void doTestDecodeUnsignedShortSeries(int size, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < size; ++i) {
+            encoder.writeUnsignedShort(buffer, encoderState, (byte)(i % 255));
+        }
+
+        for (int i = 0; i < size; ++i) {
+            final UnsignedShort result;
+            if (fromStream) {
+                result = streamDecoder.readUnsignedShort(stream, streamDecoderState);
+            } else {
+                result = decoder.readUnsignedShort(buffer, decoderState);
+            }
+
+            assertNotNull(result);
+            assertEquals((byte)(i % 255), result.byteValue());
+        }
+    }
+
+    @Test
+    public void testArrayOfObjects() throws IOException {
+        testArrayOfObjects(false);
+    }
+
+    @Test
+    public void testArrayOfObjectsFS() throws IOException {
+        testArrayOfObjects(true);
+    }
+
+    private void testArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final int size = 10;
+
+        UnsignedShort[] source = new UnsignedShort[size];
+        for (int i = 0; i < size; ++i) {
+            source[i] = UnsignedShort.valueOf((byte) (i % 255));
+        }
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedShort[] array = (UnsignedShort[]) result;
+        assertEquals(size, array.length);
+
+        for (int i = 0; i < size; ++i) {
+            assertEquals(source[i], array[i]);
+        }
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjects() throws IOException {
+        testZeroSizedArrayOfObjects(false);
+    }
+
+    @Test
+    public void testZeroSizedArrayOfObjectsFS() throws IOException {
+        testZeroSizedArrayOfObjects(true);
+    }
+
+    private void testZeroSizedArrayOfObjects(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        UnsignedShort[] source = new UnsignedShort[0];
+
+        encoder.writeArray(buffer, encoderState, source);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result.getClass().isArray());
+        assertFalse(result.getClass().getComponentType().isPrimitive());
+
+        UnsignedShort[] array = (UnsignedShort[]) result;
+        assertEquals(source.length, array.length);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    public void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeUnsignedShort(buffer, encoderState, UnsignedShort.valueOf(i));
+        }
+
+        UnsignedShort expected = UnsignedShort.valueOf(42);
+
+        encoder.writeObject(buffer, encoderState, expected);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(UnsignedShort.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.USHORT & 0xFF);
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(UnsignedShort.class, typeDecoder.getTypeClass());
+                assertTrue(typeDecoder instanceof PrimitiveTypeDecoder);
+                assertEquals(((PrimitiveTypeDecoder<?>) typeDecoder).getTypeCode(), EncodingCodes.USHORT & 0xFF);
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof UnsignedShort);
+
+        UnsignedShort value = (UnsignedShort) result;
+        assertEquals(expected, value);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslChallengeTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslChallengeTypeCodecTest.java
new file mode 100644
index 0000000..ca736ff
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslChallengeTypeCodecTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslChallengeTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslChallengeTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.security.SaslChallenge;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SaslChallengeTypeCodecTest extends CodecTestSupport {
+
+    @Override
+    @BeforeEach
+    public void setUp() {
+        decoder = ProtonDecoderFactory.createSasl();
+        decoderState = decoder.newDecoderState();
+
+        encoder = ProtonEncoderFactory.createSasl();
+        encoderState = encoder.newEncoderState();
+
+        streamDecoder = ProtonStreamDecoderFactory.createSasl();
+        streamDecoderState = streamDecoder.newDecoderState();
+    }
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(SaslChallenge.class, new SaslChallengeTypeDecoder().getTypeClass());
+        assertEquals(SaslChallenge.class, new SaslChallengeTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        SaslChallengeTypeDecoder decoder = new SaslChallengeTypeDecoder();
+        SaslChallengeTypeEncoder encoder = new SaslChallengeTypeEncoder();
+
+        assertEquals(SaslChallenge.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(SaslChallenge.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(SaslChallenge.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(SaslChallenge.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        testEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        testEncodeDecodeType(true);
+    }
+
+    private void testEncodeDecodeType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] challenge = new byte[] { 1, 2, 3, 4 };
+
+        SaslChallenge input = new SaslChallenge();
+        input.setChallenge(new Binary(challenge));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslChallenge result;
+        if (fromStream) {
+            result = (SaslChallenge) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslChallenge) decoder.readObject(buffer, decoderState);
+        }
+
+        assertArrayEquals(challenge, result.getChallenge().getArray());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslChallenge challenge = new SaslChallenge();
+
+        challenge.setChallenge(new Binary(new byte[] {0}));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, challenge);
+        }
+
+        challenge.setChallenge(new Binary(new byte[] {1, 2}));
+
+        encoder.writeObject(buffer, encoderState, challenge);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(SaslChallenge.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(SaslChallenge.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final SaslChallenge result;
+        if (fromStream) {
+            result = (SaslChallenge) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslChallenge) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof SaslChallenge);
+
+        SaslChallenge value = result;
+        assertArrayEquals(new byte[] {1, 2}, value.getChallenge().getArray());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslChallenge.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(SaslChallenge.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(SaslChallenge.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslChallenge.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslChallenge.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslChallenge[] array = new SaslChallenge[3];
+
+        array[0] = new SaslChallenge();
+        array[1] = new SaslChallenge();
+        array[2] = new SaslChallenge();
+
+        array[0].setChallenge(new Binary(new byte[] {0}));
+        array[1].setChallenge(new Binary(new byte[] {1}));
+        array[2].setChallenge(new Binary(new byte[] {2}));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(SaslChallenge.class, result.getClass().getComponentType());
+
+        SaslChallenge[] resultArray = (SaslChallenge[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof SaslChallenge);
+            assertEquals(array[i].getChallenge(), resultArray[i].getChallenge());
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslInitTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslInitTypeCodecTest.java
new file mode 100644
index 0000000..3d5c819
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslInitTypeCodecTest.java
@@ -0,0 +1,560 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslInitTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslInitTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SaslInitTypeCodecTest extends CodecTestSupport {
+
+    @Override
+    @BeforeEach
+    public void setUp() {
+        decoder = ProtonDecoderFactory.createSasl();
+        decoderState = decoder.newDecoderState();
+
+        encoder = ProtonEncoderFactory.createSasl();
+        encoderState = encoder.newEncoderState();
+
+        streamDecoder = ProtonStreamDecoderFactory.createSasl();
+        streamDecoderState = streamDecoder.newDecoderState();
+    }
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(SaslInit.class, new SaslInitTypeDecoder().getTypeClass());
+        assertEquals(SaslInit.class, new SaslInitTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        SaslInitTypeDecoder decoder = new SaslInitTypeDecoder();
+        SaslInitTypeEncoder encoder = new SaslInitTypeEncoder();
+
+        assertEquals(SaslInit.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(SaslInit.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(SaslInit.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(SaslInit.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeMechanismOnly() throws Exception {
+        doTestEncodeDecodeTypeMechanismOnly(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeMechanismOnlyFromStream() throws Exception {
+        doTestEncodeDecodeTypeMechanismOnly(true);
+    }
+
+    private void doTestEncodeDecodeTypeMechanismOnly(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslInit input = new SaslInit();
+        input.setMechanism(Symbol.valueOf("ANONYMOUS"));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslInit result;
+        if (fromStream) {
+            result = (SaslInit) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslInit) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Symbol.valueOf("ANONYMOUS"), result.getMechanism());
+        assertNull(result.getHostname());
+        assertNull(result.getInitialResponse());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithoutHostname() throws Exception {
+        doTestEncodeDecodeTypeWithoutHostname(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithoutHostnameFromStream() throws Exception {
+        doTestEncodeDecodeTypeWithoutHostname(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithoutHostname(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] initialResponse = new byte[] { 1, 2, 3, 4 };
+
+        SaslInit input = new SaslInit();
+        input.setMechanism(Symbol.valueOf("ANONYMOUS"));
+        input.setInitialResponse(new Binary(initialResponse));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslInit result;
+        if (fromStream) {
+            result = (SaslInit) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslInit) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Symbol.valueOf("ANONYMOUS"), result.getMechanism());
+        assertNull(result.getHostname());
+        assertArrayEquals(initialResponse, result.getInitialResponse().getArray());
+    }
+
+    @Test
+    public void testInitialResponseHandlesNullBinarySet() throws Exception {
+        doTestInitialResponseHandlesNullBinarySet(false);
+    }
+
+    @Test
+    public void testInitialResponseHandlesNullBinarySetFromStream() throws Exception {
+        doTestInitialResponseHandlesNullBinarySet(true);
+    }
+
+    private void doTestInitialResponseHandlesNullBinarySet(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] initialResponse = new byte[] { 1, 2, 3, 4 };
+
+        SaslInit input = new SaslInit();
+        input.setMechanism(Symbol.valueOf("ANONYMOUS"));
+
+        // Ensure that a null is handled without NPE and that it does indeed clear old value.
+        input.setInitialResponse(new Binary(initialResponse));
+        input.setInitialResponse((Binary) null);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslInit result;
+        if (fromStream) {
+            result = (SaslInit) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslInit) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Symbol.valueOf("ANONYMOUS"), result.getMechanism());
+        assertNull(result.getHostname());
+        assertNull(result.getInitialResponse());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeMechanismAndHostname() throws Exception {
+        doTestEncodeDecodeTypeMechanismAndHostname(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeMechanismAndHostnameFromStream() throws Exception {
+        doTestEncodeDecodeTypeMechanismAndHostname(true);
+    }
+
+    private void doTestEncodeDecodeTypeMechanismAndHostname(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslInit input = new SaslInit();
+        input.setMechanism(Symbol.valueOf("ANONYMOUS"));
+        input.setHostname("test");
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslInit result;
+        if (fromStream) {
+            result = (SaslInit) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslInit) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Symbol.valueOf("ANONYMOUS"), result.getMechanism());
+        assertEquals("test", result.getHostname());
+        assertNull(result.getInitialResponse());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeAllFieldsSet() throws Exception {
+        doTestEncodeDecodeTypeAllFieldsSet(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeAllFieldsSetFromStream() throws Exception {
+        doTestEncodeDecodeTypeAllFieldsSet(true);
+    }
+
+    private void doTestEncodeDecodeTypeAllFieldsSet(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] initialResponse = new byte[] { 1, 2, 3, 4 };
+
+        SaslInit input = new SaslInit();
+        input.setInitialResponse(new Binary(initialResponse));
+        input.setHostname("test");
+        input.setMechanism(Symbol.valueOf("ANONYMOUS"));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslInit result;
+        if (fromStream) {
+            result = (SaslInit) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslInit) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals("test", result.getHostname());
+        assertEquals(Symbol.valueOf("ANONYMOUS"), result.getMechanism());
+        assertArrayEquals(initialResponse, result.getInitialResponse().getArray());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslInit init = new SaslInit();
+
+        init.setInitialResponse(new Binary(new byte[] {0}));
+        init.setHostname("skip");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, init);
+        }
+
+        init.setInitialResponse(new Binary(new byte[] {1, 2}));
+        init.setHostname("localhost");
+        init.setMechanism(Symbol.valueOf("PLAIN"));
+
+        encoder.writeObject(buffer, encoderState, init);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(SaslInit.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(SaslInit.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final SaslInit result;
+        if (fromStream) {
+            result = (SaslInit) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslInit) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof SaslInit);
+
+        SaslInit value = result;
+        assertArrayEquals(new byte[] {1, 2}, value.getInitialResponse().getArray());
+        assertEquals("localhost", value.getHostname());
+        assertEquals(Symbol.valueOf("PLAIN"), value.getMechanism());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslInit.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(SaslInit.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(SaslInit.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslInit.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslInit[] array = new SaslInit[3];
+
+        array[0] = new SaslInit();
+        array[1] = new SaslInit();
+        array[2] = new SaslInit();
+
+        array[0].setInitialResponse(new Binary(new byte[] {0})).setHostname("test-1").setMechanism(Symbol.valueOf("ANONYMOUS"));
+        array[1].setInitialResponse(new Binary(new byte[] {1})).setHostname("test-2").setMechanism(Symbol.valueOf("PLAIN"));
+        array[2].setInitialResponse(new Binary(new byte[] {2})).setHostname("test-3").setMechanism(Symbol.valueOf("EXTERNAL"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(SaslInit.class, result.getClass().getComponentType());
+
+        SaslInit[] resultArray = (SaslInit[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof SaslInit);
+            assertEquals(array[i].getMechanism(), resultArray[i].getMechanism());
+            assertEquals(array[i].getHostname(), resultArray[i].getHostname());
+            assertEquals(array[i].getInitialResponse(), resultArray[i].getInitialResponse());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslInit.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(64);  // Size
+            buffer.writeInt(-1);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslInit.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 64);  // Size
+            buffer.writeInt((byte) 8);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 8);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslMechanismsTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslMechanismsTypeCodecTest.java
new file mode 100644
index 0000000..65ccfd1
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslMechanismsTypeCodecTest.java
@@ -0,0 +1,423 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslMechanismsTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslMechanismsTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslMechanisms;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SaslMechanismsTypeCodecTest extends CodecTestSupport {
+
+    @Override
+    @BeforeEach
+    public void setUp() {
+        decoder = ProtonDecoderFactory.createSasl();
+        decoderState = decoder.newDecoderState();
+
+        encoder = ProtonEncoderFactory.createSasl();
+        encoderState = encoder.newEncoderState();
+
+        streamDecoder = ProtonStreamDecoderFactory.createSasl();
+        streamDecoderState = streamDecoder.newDecoderState();
+    }
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(SaslMechanisms.class, new SaslMechanismsTypeDecoder().getTypeClass());
+        assertEquals(SaslMechanisms.class, new SaslMechanismsTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        SaslMechanismsTypeDecoder decoder = new SaslMechanismsTypeDecoder();
+        SaslMechanismsTypeEncoder encoder = new SaslMechanismsTypeEncoder();
+
+        assertEquals(SaslMechanisms.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(SaslMechanisms.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(SaslMechanisms.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(SaslMechanisms.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        doTestEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        doTestEncodeDecodeType(true);
+    }
+
+    private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Symbol[] mechanisms = new Symbol[] { Symbol.valueOf("ANONYMOUS"), Symbol.valueOf("EXTERNAL") };
+
+        SaslMechanisms input = new SaslMechanisms();
+        input.setSaslServerMechanisms(mechanisms);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslMechanisms result;
+        if (fromStream) {
+            result = (SaslMechanisms) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslMechanisms) decoder.readObject(buffer, decoderState);
+        }
+
+        assertArrayEquals(mechanisms, result.getSaslServerMechanisms());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslMechanisms mechanisms = new SaslMechanisms();
+
+        mechanisms.setSaslServerMechanisms(Symbol.valueOf("ANONYMOUS"));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, mechanisms);
+        }
+
+        mechanisms.setSaslServerMechanisms(Symbol.valueOf("ANONYMOUS"), Symbol.valueOf("EXTERNAL"));
+
+        encoder.writeObject(buffer, encoderState, mechanisms);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(SaslMechanisms.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(SaslMechanisms.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof SaslMechanisms);
+
+        SaslMechanisms value = (SaslMechanisms) result;
+        assertArrayEquals(new Symbol[] {Symbol.valueOf("ANONYMOUS"), Symbol.valueOf("EXTERNAL")}, value.getSaslServerMechanisms());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslMechanisms.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(SaslMechanisms.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(SaslMechanisms.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslMechanisms.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    public void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslMechanisms[] array = new SaslMechanisms[3];
+
+        array[0] = new SaslMechanisms();
+        array[1] = new SaslMechanisms();
+        array[2] = new SaslMechanisms();
+
+        array[0].setSaslServerMechanisms(Symbol.valueOf("ANONYMOUS"), Symbol.valueOf("PLAIN"), Symbol.valueOf("EXTERNAL"));
+        array[1].setSaslServerMechanisms(Symbol.valueOf("ANONYMOUS"), Symbol.valueOf("PLAIN"));
+        array[2].setSaslServerMechanisms(Symbol.valueOf("ANONYMOUS"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(SaslMechanisms.class, result.getClass().getComponentType());
+
+        SaslMechanisms[] resultArray = (SaslMechanisms[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof SaslMechanisms);
+            assertArrayEquals(array[i].getSaslServerMechanisms(), resultArray[i].getSaslServerMechanisms());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslMechanisms.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslMechanisms.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 64);  // Size
+            buffer.writeInt((byte) 8);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 8);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslOutcomeTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslOutcomeTypeCodecTest.java
new file mode 100644
index 0000000..2a5d039
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslOutcomeTypeCodecTest.java
@@ -0,0 +1,467 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslOutcomeTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslOutcomeTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.security.SaslCode;
+import org.apache.qpid.protonj2.types.security.SaslOutcome;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SaslOutcomeTypeCodecTest extends CodecTestSupport {
+
+    @Override
+    @BeforeEach
+    public void setUp() {
+        decoder = ProtonDecoderFactory.createSasl();
+        decoderState = decoder.newDecoderState();
+
+        encoder = ProtonEncoderFactory.createSasl();
+        encoderState = encoder.newEncoderState();
+
+        streamDecoder = ProtonStreamDecoderFactory.createSasl();
+        streamDecoderState = streamDecoder.newDecoderState();
+    }
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(SaslOutcome.class, new SaslOutcomeTypeDecoder().getTypeClass());
+        assertEquals(SaslOutcome.class, new SaslOutcomeTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        SaslOutcomeTypeDecoder decoder = new SaslOutcomeTypeDecoder();
+        SaslOutcomeTypeEncoder encoder = new SaslOutcomeTypeEncoder();
+
+        assertEquals(SaslOutcome.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(SaslOutcome.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(SaslOutcome.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(SaslOutcome.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        doTestEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        doTestEncodeDecodeType(true);
+    }
+
+    private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] data = new byte[] { 1, 2, 3, 4 };
+        SaslCode code = SaslCode.AUTH;
+
+        SaslOutcome input = new SaslOutcome();
+        input.setAdditionalData(new Binary(data));
+        input.setCode(code);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslOutcome result;
+        if (fromStream) {
+            result = (SaslOutcome) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslOutcome) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(code, result.getCode());
+        assertArrayEquals(data, result.getAdditionalData().getArray());
+    }
+
+    @Test
+    public void testAdditionalDataHandlesNullBinaryWithoutNPEAndUpdates() throws Exception {
+        doTestAdditionalDataHandlesNullBinaryWithoutNPEAndUpdates(false);
+    }
+
+    @Test
+    public void testAdditionalDataHandlesNullBinaryWithoutNPEAndUpdatesFromStream() throws Exception {
+        doTestAdditionalDataHandlesNullBinaryWithoutNPEAndUpdates(true);
+    }
+
+    private void doTestAdditionalDataHandlesNullBinaryWithoutNPEAndUpdates(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] data = new byte[] { 1, 2, 3, 4 };
+        SaslCode code = SaslCode.AUTH;
+
+        SaslOutcome input = new SaslOutcome();
+        input.setAdditionalData(new Binary(data));
+        input.setAdditionalData((Binary) null);
+        input.setCode(code);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslOutcome result;
+        if (fromStream) {
+            result = (SaslOutcome) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslOutcome) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(code, result.getCode());
+        assertNull(result.getAdditionalData());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslOutcome outcome = new SaslOutcome();
+
+        outcome.setAdditionalData(new Binary(new byte[] {0}));
+        outcome.setCode(SaslCode.AUTH);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, outcome);
+        }
+
+        outcome.setAdditionalData(new Binary(new byte[] {1, 2}));
+        outcome.setCode(SaslCode.SYS_TEMP);
+
+        encoder.writeObject(buffer, encoderState, outcome);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(SaslOutcome.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(SaslOutcome.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof SaslOutcome);
+
+        SaslOutcome value = (SaslOutcome) result;
+        assertArrayEquals(new byte[] {1, 2}, value.getAdditionalData().getArray());
+        assertEquals(SaslCode.SYS_TEMP, value.getCode());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslOutcome.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(SaslOutcome.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(SaslOutcome.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslOutcome.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslOutcome[] array = new SaslOutcome[3];
+
+        array[0] = new SaslOutcome();
+        array[1] = new SaslOutcome();
+        array[2] = new SaslOutcome();
+
+        array[0].setCode(SaslCode.OK).setAdditionalData(new Binary(new byte[] {0}));
+        array[1].setCode(SaslCode.SYS_TEMP).setAdditionalData(new Binary(new byte[] {1}));
+        array[2].setCode(SaslCode.AUTH).setAdditionalData(new Binary(new byte[] {2}));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(SaslOutcome.class, result.getClass().getComponentType());
+
+        SaslOutcome[] resultArray = (SaslOutcome[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof SaslOutcome);
+            assertEquals(array[i].getCode(), resultArray[i].getCode());
+            assertEquals(array[i].getAdditionalData(), resultArray[i].getAdditionalData());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslOutcome.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslOutcome.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 64);  // Size
+            buffer.writeInt((byte) 8);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 8);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslResponseTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslResponseTypeCodecTest.java
new file mode 100644
index 0000000..b01f7d7
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/security/SaslResponseTypeCodecTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.ProtonDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.ProtonStreamDecoderFactory;
+import org.apache.qpid.protonj2.codec.decoders.security.SaslResponseTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.ProtonEncoderFactory;
+import org.apache.qpid.protonj2.codec.encoders.security.SaslResponseTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.security.SaslResponse;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SaslResponseTypeCodecTest extends CodecTestSupport {
+
+    @Override
+    @BeforeEach
+    public void setUp() {
+        decoder = ProtonDecoderFactory.createSasl();
+        decoderState = decoder.newDecoderState();
+
+        encoder = ProtonEncoderFactory.createSasl();
+        encoderState = encoder.newEncoderState();
+
+        streamDecoder = ProtonStreamDecoderFactory.createSasl();
+        streamDecoderState = streamDecoder.newDecoderState();
+    }
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(SaslResponse.class, new SaslResponseTypeDecoder().getTypeClass());
+        assertEquals(SaslResponse.class, new SaslResponseTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        SaslResponseTypeDecoder decoder = new SaslResponseTypeDecoder();
+        SaslResponseTypeEncoder encoder = new SaslResponseTypeEncoder();
+
+        assertEquals(SaslResponse.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(SaslResponse.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(SaslResponse.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(SaslResponse.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        doTestEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        doTestEncodeDecodeType(true);
+    }
+
+    private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] response = new byte[] { 1, 2, 3, 4 };
+
+        SaslResponse input = new SaslResponse();
+        input.setResponse(new Binary(response));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslResponse result;
+        if (fromStream) {
+            result = (SaslResponse) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslResponse) decoder.readObject(buffer, decoderState);
+        }
+
+        assertArrayEquals(response, result.getResponse().getArray());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithLargeResponseBlob() throws Exception {
+        doTestEncodeDecodeTypeWithLargeResponseBlob(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithLargeResponseBlobFromStream() throws Exception {
+        doTestEncodeDecodeTypeWithLargeResponseBlob(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithLargeResponseBlob(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] response = new byte[512];
+
+        Random rand = new Random();
+        rand.setSeed(System.currentTimeMillis());
+        rand.nextBytes(response);
+
+        SaslResponse input = new SaslResponse();
+        input.setResponse(new Binary(response));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final SaslResponse result;
+        if (fromStream) {
+            result = (SaslResponse) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (SaslResponse) decoder.readObject(buffer, decoderState);
+        }
+
+        assertArrayEquals(response, result.getResponse().getArray());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValueFromStream(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValueFromStream(true);
+    }
+
+    private void doTestSkipValueFromStream(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslResponse response = new SaslResponse();
+
+        response.setResponse(new Binary(new byte[] {0}));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, response);
+        }
+
+        response.setResponse(new Binary(new byte[] {1, 2}));
+
+        encoder.writeObject(buffer, encoderState, response);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(SaslResponse.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(SaslResponse.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof SaslResponse);
+
+        SaslResponse value = (SaslResponse) result;
+        assertArrayEquals(new byte[] {1, 2}, value.getResponse().getArray());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslResponse.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(SaslResponse.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(SaslResponse.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslResponse.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        SaslResponse[] array = new SaslResponse[3];
+
+        array[0] = new SaslResponse();
+        array[1] = new SaslResponse();
+        array[2] = new SaslResponse();
+
+        array[0].setResponse(new Binary(new byte[] {0}));
+        array[1].setResponse(new Binary(new byte[] {1}));
+        array[2].setResponse(new Binary(new byte[] {2}));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(SaslResponse.class, result.getClass().getComponentType());
+
+        SaslResponse[] resultArray = (SaslResponse[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof SaslResponse);
+            assertEquals(array[i].getResponse(), resultArray[i].getResponse());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslResponse.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(SaslResponse.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 64);  // Size
+            buffer.writeInt((byte) 8);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 8);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/CoordinatorTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/CoordinatorTypeCodecTest.java
new file mode 100644
index 0000000..c04e400
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/CoordinatorTypeCodecTest.java
@@ -0,0 +1,426 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.CoordinatorTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.CoordinatorTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Coordinator serialization
+ */
+public class CoordinatorTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Coordinator.class, new CoordinatorTypeDecoder().getTypeClass());
+        assertEquals(Coordinator.class, new CoordinatorTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        CoordinatorTypeDecoder decoder = new CoordinatorTypeDecoder();
+        CoordinatorTypeEncoder encoder = new CoordinatorTypeEncoder();
+
+        assertEquals(Coordinator.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(Coordinator.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(Coordinator.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(Coordinator.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeCoordinatorType() throws Exception {
+        testEncodeDecodeCoordinatorType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeCoordinatorTypeFromStream() throws Exception {
+        testEncodeDecodeCoordinatorType(true);
+    }
+
+    private void testEncodeDecodeCoordinatorType(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Symbol[] capabilities = new Symbol[] { Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2") };
+
+        Coordinator input = new Coordinator();
+        input.setCapabilities(capabilities);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Coordinator result;
+        if (fromStream) {
+            result = (Coordinator) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Coordinator) decoder.readObject(buffer, decoderState);
+        }
+
+        assertArrayEquals(capabilities, result.getCapabilities());
+    }
+
+    @Test
+    public void testEncodeDecodeCoordinatorTypeNoCapabilities() throws Exception {
+        testEncodeDecodeCoordinatorTypeWithNoCapabilities(false);
+    }
+
+    @Test
+    public void testEncodeDecodeCoordinatorTypeNoCapabilitiesFromStream() throws Exception {
+        testEncodeDecodeCoordinatorTypeWithNoCapabilities(true);
+    }
+
+    private void testEncodeDecodeCoordinatorTypeWithNoCapabilities(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+        Coordinator input = new Coordinator();
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Coordinator result;
+        if (fromStream) {
+            result = (Coordinator) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Coordinator) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNull(result.getCapabilities());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Coordinator coordinator = new Coordinator();
+
+        coordinator.setCapabilities(Symbol.valueOf("skip"));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, coordinator);
+        }
+
+        coordinator.setCapabilities(Symbol.valueOf("read"));
+
+        encoder.writeObject(buffer, encoderState, coordinator);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Coordinator.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Coordinator.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Coordinator result;
+        if (fromStream) {
+            result = (Coordinator) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Coordinator) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Coordinator);
+
+        Coordinator value = result;
+        assertEquals(1, value.getCapabilities().length);
+        assertEquals(Symbol.valueOf("read"), value.getCapabilities()[0]);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Coordinator.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Coordinator.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Coordinator.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Coordinator.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        try {
+            if (fromStream) {
+                streamDecoder.readObject(stream, streamDecoderState);
+            } else {
+                decoder.readObject(buffer, decoderState);
+            }
+            fail("Should have failed to decode the Coordinator");
+        } catch (DecodeException ex) {
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Coordinator[] array = new Coordinator[3];
+
+        array[0] = new Coordinator();
+        array[1] = new Coordinator();
+        array[2] = new Coordinator();
+
+        array[0].setCapabilities(Symbol.valueOf("1"));
+        array[1].setCapabilities(Symbol.valueOf("1"), Symbol.valueOf("2"));
+        array[2].setCapabilities(Symbol.valueOf("1"), Symbol.valueOf("2"), Symbol.valueOf("3"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Coordinator.class, result.getClass().getComponentType());
+
+        Coordinator[] resultArray = (Coordinator[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Coordinator);
+            assertArrayEquals(array[i].getCapabilities(), resultArray[i].getCapabilities());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Coordinator.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(64);  // Size
+            buffer.writeInt(-1);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Coordinator.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DeclareTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DeclareTypeCodecTest.java
new file mode 100644
index 0000000..2841693
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DeclareTypeCodecTest.java
@@ -0,0 +1,387 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DeclareTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.DeclareTypeEncoder;
+import org.apache.qpid.protonj2.types.transactions.Declare;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Declare serialization
+ */
+public class DeclareTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Declare.class, new DeclareTypeDecoder().getTypeClass());
+        assertEquals(Declare.class, new DeclareTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        DeclareTypeDecoder decoder = new DeclareTypeDecoder();
+        DeclareTypeEncoder encoder = new DeclareTypeEncoder();
+
+        assertEquals(Declare.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(Declare.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(Declare.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(Declare.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        doTestEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        doTestEncodeDecodeType(true);
+    }
+
+    private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Declare input = new Declare();
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Declare result;
+        if (fromStream) {
+            result = (Declare) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Declare) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNull(result.getGlobalId());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Declare declare = new Declare();
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, declare);
+        }
+
+        encoder.writeObject(buffer, encoderState, declare);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Declare.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Declare.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Declare);
+
+        Declare value = (Declare) result;
+        assertNull(value.getGlobalId());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Declare.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Declare.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Declare.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Declare.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Declare[] array = new Declare[3];
+
+        array[0] = new Declare();
+        array[1] = new Declare();
+        array[2] = new Declare();
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Declare.class, result.getClass().getComponentType());
+
+        Declare[] resultArray = (Declare[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Declare);
+            assertNull(array[i].getGlobalId());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Declare.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(64);  // Size
+            buffer.writeInt(-1);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Declare.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DeclaredTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DeclaredTypeCodecTest.java
new file mode 100644
index 0000000..59b36f2
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DeclaredTypeCodecTest.java
@@ -0,0 +1,442 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DeclaredTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.DeclaredTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.transactions.Declared;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Declared serialization
+ */
+public class DeclaredTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Declared.class, new DeclaredTypeEncoder().getTypeClass());
+        assertEquals(Declared.class, new DeclaredTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        DeclaredTypeDecoder decoder = new DeclaredTypeDecoder();
+        DeclaredTypeEncoder encoder = new DeclaredTypeEncoder();
+
+        assertEquals(Declared.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(Declared.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(Declared.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(Declared.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+   @Test
+   public void testEncodeDecodeType() throws Exception {
+       doTestEncodeDecodeType(false);
+   }
+
+   @Test
+   public void testEncodeDecodeTypeFromStream() throws Exception {
+       doTestEncodeDecodeType(true);
+   }
+
+   private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+      Declared input = new Declared();
+      input.setTxnId(new Binary(new byte[] {2, 4, 6, 8}));
+
+      encoder.writeObject(buffer, encoderState, input);
+
+      final Declared result;
+      if (fromStream) {
+          result = (Declared) streamDecoder.readObject(stream, streamDecoderState);
+      } else {
+          result = (Declared) decoder.readObject(buffer, decoderState);
+      }
+
+      assertNotNull(result.getTxnId());
+      assertNotNull(result.getTxnId().getArray());
+
+      assertArrayEquals(new byte[] {2, 4, 6, 8}, result.getTxnId().getArray());
+   }
+
+   @Test
+   public void testEncodeDecodeTypeWithLargeResponseBlob() throws Exception {
+       doTestEncodeDecodeTypeWithLargeResponseBlob(false);
+   }
+
+   @Test
+   public void testEncodeDecodeTypeWithLargeResponseBlobFromStream() throws Exception {
+       doTestEncodeDecodeTypeWithLargeResponseBlob(true);
+   }
+
+   private void doTestEncodeDecodeTypeWithLargeResponseBlob(boolean fromStream) throws Exception {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       byte[] txnId = new byte[512];
+
+       Random rand = new Random();
+       rand.setSeed(System.currentTimeMillis());
+       rand.nextBytes(txnId);
+
+       Declared input = new Declared();
+
+       input.setTxnId(new Binary(txnId));
+
+       encoder.writeObject(buffer, encoderState, input);
+
+       final Declared result;
+       if (fromStream) {
+           result = (Declared) streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = (Declared) decoder.readObject(buffer, decoderState);
+       }
+
+       assertArrayEquals(txnId, result.getTxnId().getArray());
+   }
+
+   @Test
+   public void testSkipValue() throws IOException {
+       doTestSkipValue(false);
+   }
+
+   @Test
+   public void testSkipValueFromStream() throws IOException {
+       doTestSkipValue(true);
+   }
+
+   private void doTestSkipValue(boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Declared declared = new Declared();
+
+       declared.setTxnId(new Binary(new byte[] {0}));
+
+       for (int i = 0; i < 10; ++i) {
+           encoder.writeObject(buffer, encoderState, declared);
+       }
+
+       declared.setTxnId(new Binary(new byte[] {1, 2}));
+
+       encoder.writeObject(buffer, encoderState, declared);
+
+       for (int i = 0; i < 10; ++i) {
+           if (fromStream) {
+               StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+               assertEquals(Declared.class, typeDecoder.getTypeClass());
+               typeDecoder.skipValue(stream, streamDecoderState);
+           } else {
+               TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+               assertEquals(Declared.class, typeDecoder.getTypeClass());
+               typeDecoder.skipValue(buffer, decoderState);
+           }
+       }
+
+       final Declared result;
+       if (fromStream) {
+           result = (Declared) streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = (Declared) decoder.readObject(buffer, decoderState);
+       }
+
+       assertNotNull(result);
+       assertTrue(result instanceof Declared);
+
+       Declared value = result;
+       assertArrayEquals(new byte[] {1, 2}, value.getTxnId().getArray());
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap32Type() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap8Type() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+   }
+
+   @Test
+   public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+       doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+   }
+
+   private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Declared.DESCRIPTOR_CODE.byteValue());
+       if (mapType == EncodingCodes.MAP32) {
+           buffer.writeByte(EncodingCodes.MAP32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.MAP8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       }
+
+       if (fromStream) {
+           StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+           assertEquals(Declared.class, typeDecoder.getTypeClass());
+
+           try {
+               typeDecoder.skipValue(stream, streamDecoderState);
+               fail("Should not be able to skip type with invalid encoding");
+           } catch (DecodeException ex) {}
+       } else {
+           TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+           assertEquals(Declared.class, typeDecoder.getTypeClass());
+
+           try {
+               typeDecoder.skipValue(buffer, decoderState);
+               fail("Should not be able to skip type with invalid encoding");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testDecodedWithInvalidMap32Type() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap8Type() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+   }
+
+   @Test
+   public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+   }
+
+   @Test
+   public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+       doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+   }
+
+   private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Declared.DESCRIPTOR_CODE.byteValue());
+       if (mapType == EncodingCodes.MAP32) {
+           buffer.writeByte(EncodingCodes.MAP32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.MAP8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid encoding");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid encoding");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testEncodeDecodeArray() throws IOException {
+       doTestEncodeDecodeArray(false);
+   }
+
+   @Test
+   public void testEncodeDecodeArrayFromStream() throws IOException {
+       doTestEncodeDecodeArray(true);
+   }
+
+   private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+       final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       final InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Declared[] array = new Declared[3];
+
+       array[0] = new Declared();
+       array[1] = new Declared();
+       array[2] = new Declared();
+
+       array[0].setTxnId(new Binary(new byte[] {0}));
+       array[1].setTxnId(new Binary(new byte[] {1}));
+       array[2].setTxnId(new Binary(new byte[] {2}));
+
+       encoder.writeObject(buffer, encoderState, array);
+
+       final Object result;
+       if (fromStream) {
+           result = streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = decoder.readObject(buffer, decoderState);
+       }
+
+       assertTrue(result.getClass().isArray());
+       assertEquals(Declared.class, result.getClass().getComponentType());
+
+       Declared[] resultArray = (Declared[]) result;
+
+       for (int i = 0; i < resultArray.length; ++i) {
+           assertNotNull(resultArray[i]);
+           assertTrue(resultArray[i] instanceof Declared);
+           assertEquals(array[i].getTxnId(), resultArray[i].getTxnId());
+       }
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+   }
+
+   @Test
+   public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+       doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+   }
+
+   private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Declared.DESCRIPTOR_CODE.byteValue());
+       if (listType == EncodingCodes.LIST32) {
+           buffer.writeByte(EncodingCodes.LIST32);
+           buffer.writeInt((byte) 0);  // Size
+           buffer.writeInt((byte) 0);  // Count
+       } else if (listType == EncodingCodes.LIST8) {
+           buffer.writeByte(EncodingCodes.LIST8);
+           buffer.writeByte((byte) 0);  // Size
+           buffer.writeByte((byte) 0);  // Count
+       } else {
+           buffer.writeByte(EncodingCodes.LIST0);
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       }
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList8() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList32() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+   }
+
+   @Test
+   public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+       doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+   }
+
+   private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       buffer.writeByte((byte) 0); // Described Type Indicator
+       buffer.writeByte(EncodingCodes.SMALLULONG);
+       buffer.writeByte(Declared.DESCRIPTOR_CODE.byteValue());
+       if (listType == EncodingCodes.LIST32) {
+           buffer.writeByte(EncodingCodes.LIST32);
+           buffer.writeInt(128);  // Size
+           buffer.writeInt(127);  // Count
+       } else if (listType == EncodingCodes.LIST8) {
+           buffer.writeByte(EncodingCodes.LIST8);
+           buffer.writeByte((byte) 128);  // Size
+           buffer.writeByte((byte) 127);  // Count
+       }
+
+       if (fromStream) {
+           try {
+               streamDecoder.readObject(stream, streamDecoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       } else {
+           try {
+               decoder.readObject(buffer, decoderState);
+               fail("Should not decode type with invalid min entries");
+           } catch (DecodeException ex) {}
+       }
+   }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DischargeTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DischargeTypeCodecTest.java
new file mode 100644
index 0000000..85fda9c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/DischargeTypeCodecTest.java
@@ -0,0 +1,451 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.DischargeTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.DischargeTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.transactions.Discharge;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Declare serialization
+ */
+public class DischargeTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Discharge.class, new DischargeTypeDecoder().getTypeClass());
+        assertEquals(Discharge.class, new DischargeTypeDecoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        DischargeTypeDecoder decoder = new DischargeTypeDecoder();
+        DischargeTypeEncoder encoder = new DischargeTypeEncoder();
+
+        assertEquals(Discharge.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(Discharge.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(Discharge.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(Discharge.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        doTestEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        doTestEncodeDecodeType(true);
+    }
+
+    private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Discharge input = new Discharge();
+
+        input.setFail(true);
+        input.setTxnId(new Binary(new byte[] { 8, 7, 6, 5 }));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Discharge result;
+        if (fromStream) {
+            result = (Discharge) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Discharge) decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getFail());
+
+        assertNotNull(result.getTxnId());
+        assertNotNull(result.getTxnId().getArray());
+
+        assertArrayEquals(new byte[] { 8, 7, 6, 5 }, result.getTxnId().getArray());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithLargeResponseBlob() throws Exception {
+        doTestEncodeDecodeTypeWithLargeResponseBlob(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithLargeResponseBlobFromStream() throws Exception {
+        doTestEncodeDecodeTypeWithLargeResponseBlob(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithLargeResponseBlob(boolean fromStream) throws Exception {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        byte[] txnId = new byte[512];
+
+        Random rand = new Random();
+        rand.setSeed(System.currentTimeMillis());
+        rand.nextBytes(txnId);
+
+        Discharge input = new Discharge();
+
+        input.setFail(true);
+        input.setTxnId(new Binary(txnId));
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Discharge result;
+        if (fromStream) {
+            result = (Discharge) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Discharge) decoder.readObject(buffer, decoderState);
+        }
+
+        assertArrayEquals(txnId, result.getTxnId().getArray());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Discharge discharge = new Discharge();
+
+        discharge.setTxnId(new Binary(new byte[] {0}));
+        discharge.setFail(false);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, discharge);
+        }
+
+        discharge.setTxnId(new Binary(new byte[] {1, 2}));
+        discharge.setFail(true);
+
+        encoder.writeObject(buffer, encoderState, discharge);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Discharge.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Discharge.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Discharge);
+
+        Discharge value = (Discharge) result;
+        assertArrayEquals(new byte[] {1, 2}, value.getTxnId().getArray());
+        assertTrue(value.getFail());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Discharge.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Discharge.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Discharge.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Discharge.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Discharge[] array = new Discharge[3];
+
+        array[0] = new Discharge();
+        array[1] = new Discharge();
+        array[2] = new Discharge();
+
+        array[0].setTxnId(new Binary(new byte[] {0})).setFail(true);
+        array[1].setTxnId(new Binary(new byte[] {1})).setFail(false);
+        array[2].setTxnId(new Binary(new byte[] {2})).setFail(true);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Discharge.class, result.getClass().getComponentType());
+
+        Discharge[] resultArray = (Discharge[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Discharge);
+            assertEquals(array[i].getTxnId(), resultArray[i].getTxnId());
+            assertEquals(array[i].getFail(), resultArray[i].getFail());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Discharge.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Discharge.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/TransactionStateTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/TransactionStateTypeCodecTest.java
new file mode 100644
index 0000000..41578fe
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transactions/TransactionStateTypeCodecTest.java
@@ -0,0 +1,417 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transactions.TransactionStateTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transactions.TransactionStateTypeEncoder;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for handling Declared serialization
+ */
+public class TransactionStateTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(TransactionalState.class, new TransactionStateTypeDecoder().getTypeClass());
+        assertEquals(TransactionalState.class, new TransactionStateTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws Exception {
+        TransactionStateTypeDecoder decoder = new TransactionStateTypeDecoder();
+        TransactionStateTypeEncoder encoder = new TransactionStateTypeEncoder();
+
+        assertEquals(TransactionalState.DESCRIPTOR_CODE, decoder.getDescriptorCode());
+        assertEquals(TransactionalState.DESCRIPTOR_CODE, encoder.getDescriptorCode());
+        assertEquals(TransactionalState.DESCRIPTOR_SYMBOL, decoder.getDescriptorSymbol());
+        assertEquals(TransactionalState.DESCRIPTOR_SYMBOL, encoder.getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws Exception {
+        doTestEncodeDecodeType(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws Exception {
+        doTestEncodeDecodeType(true);
+    }
+
+    private void doTestEncodeDecodeType(boolean fromStream) throws Exception {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        TransactionalState input = new TransactionalState();
+        input.setTxnId(new Binary(new byte[] { 2, 4, 6, 8 }));
+        input.setOutcome(Accepted.getInstance());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final TransactionalState result;
+        if (fromStream) {
+            result = (TransactionalState) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (TransactionalState) decoder.readObject(buffer, decoderState);
+        }
+
+        assertSame(result.getOutcome(), Accepted.getInstance());
+
+        assertNotNull(result.getTxnId());
+        assertNotNull(result.getTxnId().getArray());
+
+        assertArrayEquals(new byte[] { 2, 4, 6, 8 }, result.getTxnId().getArray());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        TransactionalState txnState = new TransactionalState();
+
+        txnState.setTxnId(new Binary(new byte[] {0}));
+        txnState.setOutcome(Accepted.getInstance());
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, txnState);
+        }
+
+        txnState.setTxnId(new Binary(new byte[] {1, 2}));
+        txnState.setOutcome(null);
+
+        encoder.writeObject(buffer, encoderState, txnState);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(TransactionalState.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(TransactionalState.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof TransactionalState);
+
+        TransactionalState value = (TransactionalState) result;
+        assertArrayEquals(new byte[] {1, 2}, value.getTxnId().getArray());
+        assertNull(value.getOutcome());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(TransactionalState.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(TransactionalState.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(TransactionalState.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(TransactionalState.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        final ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        final InputStream stream = new ProtonBufferInputStream(buffer);
+
+        TransactionalState[] array = new TransactionalState[3];
+
+        array[0] = new TransactionalState();
+        array[1] = new TransactionalState();
+        array[2] = new TransactionalState();
+
+        array[0].setTxnId(new Binary(new byte[] {0})).setOutcome(Accepted.getInstance());
+        array[1].setTxnId(new Binary(new byte[] {1})).setOutcome(Released.getInstance());
+        array[2].setTxnId(new Binary(new byte[] {2})).setOutcome(new Rejected());
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(TransactionalState.class, result.getClass().getComponentType());
+
+        TransactionalState[] resultArray = (TransactionalState[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof TransactionalState);
+            assertEquals(array[i].getTxnId(), resultArray[i].getTxnId());
+            assertEquals(array[i].getOutcome().getClass(), resultArray[i].getOutcome().getClass());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(TransactionalState.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(TransactionalState.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/AttachTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/AttachTypeCodecTest.java
new file mode 100644
index 0000000..e6dfa39
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/AttachTypeCodecTest.java
@@ -0,0 +1,548 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.AttachTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.AttachTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.messaging.Terminus;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transport.Attach;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.junit.jupiter.api.Test;
+
+public class AttachTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Attach.class, new AttachTypeDecoder().getTypeClass());
+        assertEquals(Attach.class, new AttachTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Attach.DESCRIPTOR_CODE, new AttachTypeDecoder().getDescriptorCode());
+        assertEquals(Attach.DESCRIPTOR_CODE, new AttachTypeEncoder().getDescriptorCode());
+        assertEquals(Attach.DESCRIPTOR_SYMBOL, new AttachTypeDecoder().getDescriptorSymbol());
+        assertEquals(Attach.DESCRIPTOR_SYMBOL, new AttachTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testCannotEncodeEmptyPerformative() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Attach input = new Attach();
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+            fail("Cannot omit required fields.");
+        } catch (EncodeException encEx) {
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithTarget() throws Exception {
+        doTestEncodeDecodeType(new Target(), false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithCoordinator() throws Exception {
+        doTestEncodeDecodeType(new Coordinator(), false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithTargetFromStream() throws Exception {
+        doTestEncodeDecodeType(new Target(), true);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithCoordinatorFromStream() throws Exception {
+        doTestEncodeDecodeType(new Coordinator(), true);
+    }
+
+    private void doTestEncodeDecodeType(Terminus target, boolean fromStream) throws Exception {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+       Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+
+       final Random random = new Random();
+       random.setSeed(System.nanoTime());
+
+       final int randomHandle = random.nextInt();
+       final int randomInitialDeliveryCount = random.nextInt();
+
+       Attach input = new Attach();
+
+       input.setName("name");
+       input.setOfferedCapabilities(offeredCapabilities);
+       input.setDesiredCapabilities(desiredCapabilities);
+       input.setHandle(randomHandle);
+       input.setRole(Role.RECEIVER);
+       input.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+       input.setReceiverSettleMode(ReceiverSettleMode.SECOND);
+       input.setSource(new Source());
+       input.setTarget(target);
+       input.setIncompleteUnsettled(false);
+       input.setInitialDeliveryCount(randomInitialDeliveryCount);
+       input.setMaxMessageSize(UnsignedLong.valueOf(1024));
+
+       encoder.writeObject(buffer, encoderState, input);
+
+       final Attach result;
+       if (fromStream) {
+           result = (Attach) streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = (Attach) decoder.readObject(buffer, decoderState);
+       }
+
+       assertEquals("name", result.getName());
+       assertEquals(Integer.toUnsignedLong(randomHandle), result.getHandle());
+       assertEquals(Role.RECEIVER, result.getRole());
+       assertEquals(SenderSettleMode.UNSETTLED, result.getSenderSettleMode());
+       assertEquals(ReceiverSettleMode.SECOND, result.getReceiverSettleMode());
+       assertEquals(Integer.toUnsignedLong(randomInitialDeliveryCount), result.getInitialDeliveryCount());
+       assertEquals(UnsignedLong.valueOf(1024), result.getMaxMessageSize());
+       assertNotNull(result.getSource());
+       assertNotNull(result.getTarget());
+       assertFalse(result.getIncompleteUnsettled());
+       assertNull(result.getUnsettled());
+       assertNull(result.getProperties());
+       assertArrayEquals(offeredCapabilities, result.getOfferedCapabilities());
+       assertArrayEquals(desiredCapabilities, result.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testEncodeUsingNewCodecAndDecodeWithLegacyCodec() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+
+        Attach input = new Attach();
+
+        input.setName("name");
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+        input.setHandle(64);
+        input.setRole(Role.RECEIVER);
+        input.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+        input.setReceiverSettleMode(ReceiverSettleMode.SECOND);
+        input.setSource(new Source());
+        input.setTarget(new Target());
+        input.setIncompleteUnsettled(false);
+        input.setInitialDeliveryCount(10);
+        input.setMaxMessageSize(UnsignedLong.valueOf(1024));
+
+        encoder.writeObject(buffer, encoderState, input);
+        Object decoded = legacyCodec.decodeLegacyType(buffer);
+        assertTrue(decoded instanceof Attach);
+        final Attach result = (Attach) decoded;
+        assertNotNull(result);
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodec() throws Exception {
+        testEncodeUsingLegacyCodecAndDecodeWithNewCodec(false);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodecFromStream() throws Exception {
+        testEncodeUsingLegacyCodecAndDecodeWithNewCodec(true);
+    }
+
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodec(boolean fromStream) throws Exception {
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+
+        Attach input = new Attach();
+
+        input.setName("name");
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+        input.setHandle(64);
+        input.setRole(Role.RECEIVER);
+        input.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+        input.setReceiverSettleMode(ReceiverSettleMode.SECOND);
+        input.setSource(new Source());
+        input.setTarget(new Target());
+        input.setIncompleteUnsettled(false);
+        input.setInitialDeliveryCount(10);
+        input.setMaxMessageSize(UnsignedLong.valueOf(1024));
+
+        ProtonBuffer buffer = legacyCodec.encodeUsingLegacyEncoder(input);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        assertNotNull(buffer);
+
+        final Attach result;
+        if (fromStream) {
+            result = (Attach) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Attach) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Attach attach = new Attach();
+
+        attach.setHandle(1);
+        attach.setRole(Role.RECEIVER);
+        attach.setName("skip");
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, attach);
+        }
+
+        attach.setHandle(2);
+        attach.setRole(Role.SENDER);
+        attach.setName("test");
+
+        encoder.writeObject(buffer, encoderState, attach);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Attach.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Attach.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Attach);
+
+        Attach value = (Attach) result;
+        assertEquals(Role.SENDER, value.getRole());
+        assertEquals(2, value.getHandle());
+        assertEquals("test", value.getName());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Attach.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Attach.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Attach.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Attach.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Attach[] array = new Attach[3];
+
+        array[0] = new Attach();
+        array[1] = new Attach();
+        array[2] = new Attach();
+
+        array[0].setHandle(0).setName("0").setInitialDeliveryCount(0).setRole(Role.SENDER);
+        array[1].setHandle(1).setName("1").setInitialDeliveryCount(1).setRole(Role.SENDER);
+        array[2].setHandle(2).setName("2").setInitialDeliveryCount(2).setRole(Role.SENDER);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Attach.class, result.getClass().getComponentType());
+
+        Attach[] resultArray = (Attach[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Attach);
+            assertEquals(array[i].getHandle(), resultArray[i].getHandle());
+            assertEquals(array[i].getName(), resultArray[i].getName());
+            assertEquals(array[i].getInitialDeliveryCount(), resultArray[i].getInitialDeliveryCount());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Attach.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Attach.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/BeginTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/BeginTypeCodecTest.java
new file mode 100644
index 0000000..d23bf7e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/BeginTypeCodecTest.java
@@ -0,0 +1,574 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.BeginTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.BeginTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.Begin;
+import org.junit.jupiter.api.Test;
+
+public class BeginTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Begin.class, new BeginTypeDecoder().getTypeClass());
+        assertEquals(Begin.class, new BeginTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Begin.DESCRIPTOR_CODE, new BeginTypeDecoder().getDescriptorCode());
+        assertEquals(Begin.DESCRIPTOR_CODE, new BeginTypeEncoder().getDescriptorCode());
+        assertEquals(Begin.DESCRIPTOR_SYMBOL, new BeginTypeDecoder().getDescriptorSymbol());
+        assertEquals(Begin.DESCRIPTOR_SYMBOL, new BeginTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testCannotEncodeEmptyPerformative() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Begin input = new Begin();
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+            fail("Cannot omit required fields.");
+        } catch (EncodeException encEx) {
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeType() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Symbol[] offeredCapabilities = new Symbol[] { Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2") };
+        Symbol[] desiredCapabilities = new Symbol[] { Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4") };
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("property"), "value");
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomChannel = random.nextInt(65535);
+        final int randomeNextOutgoingId = random.nextInt();
+        final int randomeNextIncomingWindow = random.nextInt();
+        final int randomeNextOutgoingWindow = random.nextInt();
+        final int randomeHandleMax = random.nextInt();
+
+        Begin input = new Begin();
+
+        input.setRemoteChannel(randomChannel);
+        input.setNextOutgoingId(randomeNextOutgoingId);
+        input.setIncomingWindow(randomeNextIncomingWindow);
+        input.setOutgoingWindow(randomeNextOutgoingWindow);
+        input.setHandleMax(randomeHandleMax);
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+        input.setProperties(properties);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Begin result;
+        if (fromStream) {
+            result = (Begin) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Begin) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(randomChannel, result.getRemoteChannel());
+        assertEquals(Integer.toUnsignedLong(randomeNextOutgoingId), result.getNextOutgoingId());
+        assertEquals(Integer.toUnsignedLong(randomeNextIncomingWindow), result.getIncomingWindow());
+        assertEquals(Integer.toUnsignedLong(randomeNextOutgoingWindow), result.getOutgoingWindow());
+        assertEquals(Integer.toUnsignedLong(randomeHandleMax), result.getHandleMax());
+        assertNotNull(result.getProperties());
+        assertEquals(1, properties.size());
+        assertTrue(properties.containsKey(Symbol.valueOf("property")));
+        assertArrayEquals(offeredCapabilities, result.getOfferedCapabilities());
+        assertArrayEquals(desiredCapabilities, result.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testEncodeDecodeFailsOnMissingIncomingWindow() throws IOException {
+        testEncodeDecodeFailsOnMissingIncomingWindow(false);
+    }
+
+    @Test
+    public void testEncodeDecodeFailsOnMissingIncomingWindowFromStream() throws IOException {
+        testEncodeDecodeFailsOnMissingIncomingWindow(true);
+    }
+
+    private void testEncodeDecodeFailsOnMissingIncomingWindow(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomChannel = random.nextInt(65535);
+        final int randomeNextOutgoingId = random.nextInt();
+        final int randomeNextOutgoingWindow = random.nextInt();
+        final int randomeHandleMax = random.nextInt();
+
+        Begin input = new Begin();
+
+        input.setRemoteChannel(randomChannel);
+        input.setNextOutgoingId(randomeNextOutgoingId);
+        input.setOutgoingWindow(randomeNextOutgoingWindow);
+        input.setHandleMax(randomeHandleMax);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeUsingNewCodecAndDecodeWithLegacyCodec() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+        Map<Symbol, Object> properties = new LinkedHashMap<>();
+        properties.put(Symbol.valueOf("property"), "value");
+
+        Begin input = new Begin();
+
+        input.setRemoteChannel(16);
+        input.setNextOutgoingId(24);
+        input.setIncomingWindow(32);
+        input.setOutgoingWindow(12);
+        input.setHandleMax(255);
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+        input.setProperties(properties);
+
+        encoder.writeObject(buffer, encoderState, input);
+        Object decoded = legacyCodec.decodeLegacyType(buffer);
+        assertTrue(decoded instanceof Begin);
+        final Begin result = (Begin) decoded;
+
+        assertNotNull(result);
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodec() throws Exception {
+        doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(false);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodecFromStream() throws Exception {
+        doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(true);
+    }
+
+    private void doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(boolean fromStream) throws Exception {
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("property"), "value");
+
+        Begin input = new Begin();
+
+        input.setRemoteChannel(16);
+        input.setNextOutgoingId(24);
+        input.setIncomingWindow(32);
+        input.setOutgoingWindow(12);
+        input.setHandleMax(255);
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+        input.setProperties(properties);
+
+        ProtonBuffer buffer = legacyCodec.encodeUsingLegacyEncoder(input);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        assertNotNull(buffer);
+
+        final Begin result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState, Begin.class);
+        } else {
+            result = decoder.readObject(buffer, decoderState, Begin.class);
+        }
+
+        assertNotNull(result);
+
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Begin begin = new Begin();
+
+        begin.setRemoteChannel(1);
+        begin.setNextOutgoingId(0);
+        begin.setIncomingWindow(1024);
+        begin.setOutgoingWindow(1024);
+        begin.setHandleMax(25);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, begin);
+        }
+
+        begin.setRemoteChannel(2);
+        begin.setNextOutgoingId(0);
+        begin.setIncomingWindow(1024);
+        begin.setOutgoingWindow(1024);
+        begin.setHandleMax(50);
+
+        encoder.writeObject(buffer, encoderState, begin);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Begin.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Begin.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Begin);
+
+        Begin value = (Begin) result;
+        assertEquals(2, value.getRemoteChannel());
+        assertEquals(50, value.getHandleMax());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Begin.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Begin.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Begin.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Begin.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Begin[] array = new Begin[3];
+
+        array[0] = new Begin();
+        array[1] = new Begin();
+        array[2] = new Begin();
+
+        array[0].setNextOutgoingId(0).setRemoteChannel(0).setIncomingWindow(0).setOutgoingWindow(0);
+        array[1].setNextOutgoingId(1).setRemoteChannel(1).setIncomingWindow(1).setOutgoingWindow(1);
+        array[2].setNextOutgoingId(2).setRemoteChannel(2).setIncomingWindow(2).setOutgoingWindow(2);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Begin.class, result.getClass().getComponentType());
+
+        Begin[] resultArray = (Begin[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Begin);
+            assertEquals(array[i].getNextOutgoingId(), resultArray[i].getNextOutgoingId());
+            assertEquals(array[i].getOutgoingWindow(), resultArray[i].getOutgoingWindow());
+            assertEquals(array[i].getIncomingWindow(), resultArray[i].getIncomingWindow());
+            assertEquals(array[i].getRemoteChannel(), resultArray[i].getRemoteChannel());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Begin.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Begin.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/CloseTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/CloseTypeCodecTest.java
new file mode 100644
index 0000000..446387c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/CloseTypeCodecTest.java
@@ -0,0 +1,523 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.CloseTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.CloseTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.Close;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+public class CloseTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Close.class, new CloseTypeDecoder().getTypeClass());
+        assertEquals(Close.class, new CloseTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Close.DESCRIPTOR_CODE, new CloseTypeDecoder().getDescriptorCode());
+        assertEquals(Close.DESCRIPTOR_CODE, new CloseTypeEncoder().getDescriptorCode());
+        assertEquals(Close.DESCRIPTOR_SYMBOL, new CloseTypeDecoder().getDescriptorSymbol());
+        assertEquals(Close.DESCRIPTOR_SYMBOL, new CloseTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithNoError() throws IOException {
+        doTestEncodeDecodeTypeWithNoError(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithNoErrorFromStream() throws IOException {
+        doTestEncodeDecodeTypeWithNoError(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithNoError(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Close input = new Close();
+
+       encoder.writeObject(buffer, encoderState, input);
+
+       final Close result;
+       if (fromStream) {
+           result = (Close) streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = (Close) decoder.readObject(buffer, decoderState);
+       }
+
+       assertNull(result.getError());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithError() throws Exception {
+        doTestEncodeDecodeTypeWithError(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithErrorFromStream() throws Exception {
+        doTestEncodeDecodeTypeWithError(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithError(boolean fromStream) throws Exception {
+       ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+       ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), null);
+       InputStream stream = new ProtonBufferInputStream(buffer);
+
+       Close input = new Close();
+
+       input.setError(error);
+
+       encoder.writeObject(buffer, encoderState, input);
+
+       final Close result;
+       if (fromStream) {
+           result = (Close) streamDecoder.readObject(stream, streamDecoderState);
+       } else {
+           result = (Close) decoder.readObject(buffer, decoderState);
+       }
+
+       assertNotNull(result.getError());
+       assertNotNull(result.getError().getCondition());
+       assertNull(result.getError().getDescription());
+    }
+
+    @Test
+    public void testEncodeUsingNewCodecAndDecodeWithLegacyCodec() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), "error message");
+
+        Close input = new Close();
+
+        input.setError(error);
+
+        encoder.writeObject(buffer, encoderState, input);
+        Object decoded = legacyCodec.decodeLegacyType(buffer);
+        assertTrue(decoded instanceof Close);
+        final Close result = (Close) decoded;
+
+        assertNotNull(result);
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testEncodeEmptyUsingNewCodecAndDecodeWithLegacyCodec() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Close input = new Close();
+
+        encoder.writeObject(buffer, encoderState, input);
+        Object decoded = legacyCodec.decodeLegacyType(buffer);
+        assertTrue(decoded instanceof Close);
+        final Close result = (Close) decoded;
+
+        assertNotNull(result);
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodec() throws Exception {
+        doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(false);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodecFromStream() throws Exception {
+        doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(true);
+    }
+
+    private void doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(boolean fromStream) throws Exception {
+        ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), "error message");
+
+        Close input = new Close();
+
+        input.setError(error);
+
+        ProtonBuffer buffer = legacyCodec.encodeUsingLegacyEncoder(input);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        assertNotNull(buffer);
+
+        final Close result;
+        if (fromStream) {
+            result = (Close) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Close) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testEncodeEmptyUsingLegacyCodecAndDecodeWithNewCodec() throws Exception {
+        doTestEncodeEmptyUsingLegacyCodecAndDecodeWithNewCodec(false);
+    }
+
+    @Test
+    public void testEncodeEmptyUsingLegacyCodecAndDecodeWithNewCodecFromStream() throws Exception {
+        doTestEncodeEmptyUsingLegacyCodecAndDecodeWithNewCodec(true);
+    }
+
+    private void doTestEncodeEmptyUsingLegacyCodecAndDecodeWithNewCodec(boolean fromStream) throws Exception {
+        Close input = new Close();
+
+        ProtonBuffer buffer = legacyCodec.encodeUsingLegacyEncoder(input);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        assertNotNull(buffer);
+
+        final Close result;
+        if (fromStream) {
+            result = (Close) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Close) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Close close = new Close();
+
+        close.setError(new ErrorCondition(AmqpError.INVALID_FIELD, "test"));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, close);
+        }
+
+        close.setError(null);
+
+        encoder.writeObject(buffer, encoderState, close);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Close.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Close.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Close);
+
+        Close value = (Close) result;
+        assertNull(value.getError());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Close.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Close.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Close.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Close.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Close[] array = new Close[3];
+
+        array[0] = new Close();
+        array[1] = new Close();
+        array[2] = new Close();
+
+        array[0].setError(new ErrorCondition(AmqpError.DECODE_ERROR, "1"));
+        array[1].setError(new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, "2"));
+        array[2].setError(new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "3"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Close.class, result.getClass().getComponentType());
+
+        Close[] resultArray = (Close[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Close);
+            assertEquals(array[i].getError(), resultArray[i].getError());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Close.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(64);  // Size
+            buffer.writeInt(-1);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Close.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/DetachTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/DetachTypeCodecTest.java
new file mode 100644
index 0000000..c345f10
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/DetachTypeCodecTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.DetachTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.DetachTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.Detach;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+public class DetachTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Detach.class, new DetachTypeDecoder().getTypeClass());
+        assertEquals(Detach.class, new DetachTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Detach.DESCRIPTOR_CODE, new DetachTypeDecoder().getDescriptorCode());
+        assertEquals(Detach.DESCRIPTOR_CODE, new DetachTypeEncoder().getDescriptorCode());
+        assertEquals(Detach.DESCRIPTOR_SYMBOL, new DetachTypeDecoder().getDescriptorSymbol());
+        assertEquals(Detach.DESCRIPTOR_SYMBOL, new DetachTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testCannotEncodeEmptyPerformative() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Detach input = new Detach();
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+            fail("Cannot omit required fields.");
+        } catch (EncodeException encEx) {
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithNoError() throws IOException {
+        doTestEncodeDecodeTypeWithNoError(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithNoErrorFromStream() throws IOException {
+        doTestEncodeDecodeTypeWithNoError(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithNoError(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Detach input = new Detach();
+        input.setHandle(1);
+        input.setClosed(false);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Detach result;
+        if (fromStream) {
+            result = (Detach) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Detach) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(1, result.getHandle());
+        assertFalse(result.getClosed());
+        assertNull(result.getError());
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithError() throws Exception {
+        doTestEncodeDecodeTypeWithError(false);
+    }
+
+    @Test
+    public void testEncodeDecodeTypeWithErrorFromStream() throws Exception {
+        doTestEncodeDecodeTypeWithError(true);
+    }
+
+    private void doTestEncodeDecodeTypeWithError(boolean fromStream) throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), null);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Detach input = new Detach();
+        input.setHandle(1);
+        input.setClosed(true);
+        input.setError(error);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Detach result;
+        if (fromStream) {
+            result = (Detach) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Detach) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(1, result.getHandle());
+        assertTrue(result.getClosed());
+        assertNotNull(result.getError());
+        assertNotNull(result.getError().getCondition());
+        assertNull(result.getError().getDescription());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Detach detach = new Detach();
+
+        detach.setHandle(1);
+        detach.setError(new ErrorCondition(AmqpError.INVALID_FIELD, "test"));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, detach);
+        }
+
+        detach.setError(null);
+
+        encoder.writeObject(buffer, encoderState, detach);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Detach.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Detach.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Detach);
+
+        Detach value = (Detach) result;
+        assertNull(value.getError());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Detach.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Detach.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Detach.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Detach.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Detach[] array = new Detach[3];
+
+        array[0] = new Detach().setHandle(0);
+        array[1] = new Detach().setHandle(1);
+        array[2] = new Detach().setHandle(2);
+
+        array[0].setError(new ErrorCondition(AmqpError.DECODE_ERROR, "1"));
+        array[1].setError(new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, "2"));
+        array[2].setError(new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "3"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Detach.class, result.getClass().getComponentType());
+
+        Detach[] resultArray = (Detach[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Detach);
+            assertEquals(array[i].getError(), resultArray[i].getError());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Detach.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Detach.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/DispositionTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/DispositionTypeCodecTest.java
new file mode 100644
index 0000000..154ff0b
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/DispositionTypeCodecTest.java
@@ -0,0 +1,515 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.DispositionTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.DispositionTypeEncoder;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.transport.Disposition;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.junit.jupiter.api.Test;
+
+public class DispositionTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Disposition.class, new DispositionTypeDecoder().getTypeClass());
+        assertEquals(Disposition.class, new DispositionTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Disposition.DESCRIPTOR_CODE, new DispositionTypeDecoder().getDescriptorCode());
+        assertEquals(Disposition.DESCRIPTOR_CODE, new DispositionTypeEncoder().getDescriptorCode());
+        assertEquals(Disposition.DESCRIPTOR_SYMBOL, new DispositionTypeDecoder().getDescriptorSymbol());
+        assertEquals(Disposition.DESCRIPTOR_SYMBOL, new DispositionTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testCannotEncodeEmptyPerformative() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Disposition input = new Disposition();
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+            fail("Cannot omit required fields.");
+        } catch (EncodeException encEx) {
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecodeWithNullState() throws IOException {
+        doTestEncodeAndDecodeWithNullState(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeWithNullStateFromStream() throws IOException {
+        doTestEncodeAndDecodeWithNullState(true);
+    }
+
+    private void doTestEncodeAndDecodeWithNullState(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Disposition input = new Disposition();
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomFirst = random.nextInt();
+        final int randomLast = random.nextInt();
+
+        input.setFirst(randomFirst);
+        input.setLast(randomLast);
+        input.setRole(Role.RECEIVER);
+        input.setBatchable(false);
+        input.setSettled(true);
+        input.setState(null);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Disposition result;
+        if (fromStream) {
+            result = (Disposition) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Disposition) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Integer.toUnsignedLong(randomFirst), result.getFirst());
+        assertEquals(Integer.toUnsignedLong(randomLast), result.getLast());
+        assertEquals(Role.RECEIVER, result.getRole());
+        assertEquals(false, result.getBatchable());
+        assertEquals(true, result.getSettled());
+        assertNull(result.getState());
+    }
+
+    @Test
+    public void testEncodeAndDecode() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Disposition input = new Disposition();
+
+        input.setFirst(1);
+        input.setRole(Role.RECEIVER);
+        input.setBatchable(false);
+        input.setSettled(true);
+        input.setState(Accepted.getInstance());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Disposition result;
+        if (fromStream) {
+            result = (Disposition) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Disposition) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(1, result.getFirst());
+        assertEquals(Role.RECEIVER, result.getRole());
+        assertEquals(false, result.getBatchable());
+        assertEquals(true, result.getSettled());
+        assertSame(Accepted.getInstance(), result.getState());
+    }
+
+    @Test
+    public void testDecodeEnforcesFirstValueRequired() throws IOException {
+        doTestDecodeEnforcesFirstValueRequired(false);
+    }
+
+    @Test
+    public void testDecodeEnforcesFirstValueRequiredFromStream() throws IOException {
+        doTestDecodeEnforcesFirstValueRequired(true);
+    }
+
+    private void doTestDecodeEnforcesFirstValueRequired(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Disposition input = new Disposition();
+
+        input.setRole(Role.RECEIVER);
+        input.setSettled(true);
+        input.setState(Accepted.getInstance());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not encode when no First value is set");
+            } catch (Exception ex) {
+            }
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not encode when no First value is set");
+            } catch (Exception ex) {
+            }
+        }
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Disposition disposition = new Disposition();
+
+        disposition.setFirst(1);
+        disposition.setLast(2);
+        disposition.setRole(Role.RECEIVER);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, disposition);
+        }
+
+        disposition.setFirst(2);
+        disposition.setLast(3);
+        disposition.setRole(Role.SENDER);
+        disposition.setState(Accepted.getInstance());
+
+        encoder.writeObject(buffer, encoderState, disposition);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Disposition.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Disposition.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Disposition);
+
+        Disposition value = (Disposition) result;
+        assertEquals(2, value.getFirst());
+        assertEquals(3, value.getLast());
+        assertEquals(Role.SENDER, value.getRole());
+        assertSame(Accepted.getInstance(), value.getState());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Disposition.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Disposition.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Disposition.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Disposition.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Disposition[] array = new Disposition[3];
+
+        array[0] = new Disposition();
+        array[1] = new Disposition();
+        array[2] = new Disposition();
+
+        array[0].setFirst(0).setRole(Role.SENDER).setSettled(true);
+        array[1].setFirst(1).setRole(Role.RECEIVER).setSettled(false);
+        array[2].setFirst(2).setRole(Role.SENDER).setSettled(true);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Disposition.class, result.getClass().getComponentType());
+
+        Disposition[] resultArray = (Disposition[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Disposition);
+            assertEquals(array[i].getFirst(), resultArray[i].getFirst());
+            assertEquals(array[i].getRole(), resultArray[i].getRole());
+            assertEquals(array[i].getSettled(), resultArray[i].getSettled());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Disposition.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Disposition.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/EndTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/EndTypeCodecTest.java
new file mode 100644
index 0000000..44df6f4
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/EndTypeCodecTest.java
@@ -0,0 +1,410 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.EndTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.EndTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.End;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+public class EndTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(End.class, new EndTypeDecoder().getTypeClass());
+        assertEquals(End.class, new EndTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(End.DESCRIPTOR_CODE, new EndTypeDecoder().getDescriptorCode());
+        assertEquals(End.DESCRIPTOR_CODE, new EndTypeEncoder().getDescriptorCode());
+        assertEquals(End.DESCRIPTOR_SYMBOL, new EndTypeDecoder().getDescriptorSymbol());
+        assertEquals(End.DESCRIPTOR_SYMBOL, new EndTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeAndDecode() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> infoMap = new LinkedHashMap<>();
+        infoMap.put(Symbol.valueOf("1"), true);
+        infoMap.put(Symbol.valueOf("2"), "string");
+
+        ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), "Something bad", infoMap);
+
+        End input = new End();
+        input.setError(error);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final End result;
+        if (fromStream) {
+            result = (End) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (End) decoder.readObject(buffer, decoderState);
+        }
+
+        final ErrorCondition resultError = result.getError();
+
+        assertNotNull(resultError);
+        assertNotNull(resultError.getCondition());
+        assertNotNull(resultError.getDescription());
+        assertNotNull(resultError.getInfo());
+
+        assertEquals(Symbol.valueOf("amqp-error"), resultError.getCondition());
+        assertEquals("Something bad", resultError.getDescription());
+        assertEquals(infoMap, resultError.getInfo());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        End end = new End();
+
+        end.setError(new ErrorCondition(AmqpError.INVALID_FIELD, "test"));
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, end);
+        }
+
+        end.setError(null);
+
+        encoder.writeObject(buffer, encoderState, end);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(End.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(End.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof End);
+
+        End value = (End) result;
+        assertNull(value.getError());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(End.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(End.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(End.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(End.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        End[] array = new End[3];
+
+        array[0] = new End();
+        array[1] = new End();
+        array[2] = new End();
+
+        array[0].setError(new ErrorCondition(AmqpError.DECODE_ERROR, "1"));
+        array[1].setError(new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, "2"));
+        array[2].setError(new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "3"));
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(End.class, result.getClass().getComponentType());
+
+        End[] resultArray = (End[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof End);
+            assertEquals(array[i].getError(), resultArray[i].getError());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(End.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(64);  // Size
+            buffer.writeInt(-1);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 64);  // Size
+            buffer.writeByte((byte) 0xFF);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(End.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/ErrorConditionTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/ErrorConditionTypeCodecTest.java
new file mode 100644
index 0000000..8095f68
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/ErrorConditionTypeCodecTest.java
@@ -0,0 +1,484 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.ErrorConditionTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.ErrorConditionTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+public class ErrorConditionTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(ErrorCondition.class, new ErrorConditionTypeDecoder().getTypeClass());
+        assertEquals(ErrorCondition.class, new ErrorConditionTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(ErrorCondition.DESCRIPTOR_CODE, new ErrorConditionTypeDecoder().getDescriptorCode());
+        assertEquals(ErrorCondition.DESCRIPTOR_CODE, new ErrorConditionTypeEncoder().getDescriptorCode());
+        assertEquals(ErrorCondition.DESCRIPTOR_SYMBOL, new ErrorConditionTypeDecoder().getDescriptorSymbol());
+        assertEquals(ErrorCondition.DESCRIPTOR_SYMBOL, new ErrorConditionTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeAndDecode() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> infoMap = new LinkedHashMap<>();
+        infoMap.put(Symbol.valueOf("1"), true);
+        infoMap.put(Symbol.valueOf("2"), "string");
+
+        ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), "Something bad", infoMap);
+
+        encoder.writeObject(buffer, encoderState, error);
+
+        final ErrorCondition result;
+        if (fromStream) {
+            result = (ErrorCondition) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (ErrorCondition) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertNotNull(result.getCondition());
+        assertNotNull(result.getDescription());
+        assertNotNull(result.getInfo());
+
+        assertEquals(Symbol.valueOf("amqp-error"), result.getCondition());
+        assertEquals("Something bad", result.getDescription());
+        assertEquals(infoMap, result.getInfo());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Map<Symbol, Object> infoMap = new LinkedHashMap<>();
+        infoMap.put(Symbol.valueOf("1"), true);
+        infoMap.put(Symbol.valueOf("2"), "string");
+
+        ErrorCondition error = new ErrorCondition(Symbol.valueOf("amqp-error"), "Something bad", infoMap);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, error);
+        }
+
+        error = new ErrorCondition(Symbol.valueOf("amqp-error-2"), "Something bad also", null);
+
+        encoder.writeObject(buffer, encoderState, error);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(ErrorCondition.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(ErrorCondition.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof ErrorCondition);
+
+        ErrorCondition value = (ErrorCondition) result;
+        assertEquals(Symbol.valueOf("amqp-error-2"), value.getCondition());
+        assertEquals("Something bad also", value.getDescription());
+        assertNull(value.getInfo());
+    }
+
+    @Test
+    public void testEqualityOfNewlyConstructed() {
+        ErrorCondition new1 = new ErrorCondition(null, null, null);
+        ErrorCondition new2 = new ErrorCondition(null, null, null);
+        assertErrorConditionsEqual(new1, new2);
+    }
+
+    @Test
+    public void testSameObject() {
+        ErrorCondition error = new ErrorCondition(null, null, null);
+        assertErrorConditionsEqual(error, error);
+    }
+
+    @Test
+    public void testConditionEquality() {
+        String symbolValue = "symbol";
+
+        ErrorCondition same1 = new ErrorCondition(Symbol.valueOf(symbolValue), null);
+        ErrorCondition same2 = new ErrorCondition(Symbol.valueOf(symbolValue), null);
+
+        assertErrorConditionsEqual(same1, same2);
+
+        ErrorCondition different = new ErrorCondition(Symbol.getSymbol("other"), null);
+
+        assertErrorConditionsNotEqual(same1, different);
+    }
+
+    @Test
+    public void testConditionAndDescriptionEquality() {
+        String symbolValue = "symbol";
+        String descriptionValue = "description";
+
+        ErrorCondition same1 = new ErrorCondition(Symbol.getSymbol(new String(symbolValue)), new String(descriptionValue));
+        ErrorCondition same2 = new ErrorCondition(Symbol.getSymbol(new String(symbolValue)), new String(descriptionValue));
+
+        assertErrorConditionsEqual(same1, same2);
+
+        ErrorCondition different = new ErrorCondition(Symbol.getSymbol(symbolValue), "other");
+
+        assertErrorConditionsNotEqual(same1, different);
+    }
+
+    @Test
+    public void testConditionDescriptionInfoEquality() {
+        String symbolValue = "symbol";
+        String descriptionValue = "description";
+
+        ErrorCondition same1 = new ErrorCondition(
+            Symbol.getSymbol(new String(symbolValue)), new String(descriptionValue), Collections.singletonMap(Symbol.getSymbol("key"), "value"));
+        ErrorCondition same2 = new ErrorCondition(
+            Symbol.getSymbol(new String(symbolValue)), new String(descriptionValue), Collections.singletonMap(Symbol.getSymbol("key"), "value"));
+
+        assertErrorConditionsEqual(same1, same2);
+
+        ErrorCondition different = new ErrorCondition(
+            Symbol.getSymbol(symbolValue), new String(descriptionValue), Collections.singletonMap(Symbol.getSymbol("other"), "value"));
+
+        assertErrorConditionsNotEqual(same1, different);
+    }
+
+    private void assertErrorConditionsNotEqual(ErrorCondition error1, ErrorCondition error2) {
+        assertThat(error1, is(not(error2)));
+        assertThat(error2, is(not(error1)));
+    }
+
+    private void assertErrorConditionsEqual(ErrorCondition error1, ErrorCondition error2) {
+        assertEquals(error1, error2);
+        assertEquals(error2, error1);
+        assertEquals(error1.hashCode(), error2.hashCode());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ErrorCondition.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(ErrorCondition.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(ErrorCondition.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ErrorCondition.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ErrorCondition[] array = new ErrorCondition[3];
+
+        array[0] = new ErrorCondition(AmqpError.DECODE_ERROR, "1");
+        array[1] = new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, "2");
+        array[2] = new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "3");
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(ErrorCondition.class, result.getClass().getComponentType());
+
+        ErrorCondition[] resultArray = (ErrorCondition[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof ErrorCondition);
+            assertEquals(array[i], resultArray[i]);
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ErrorCondition.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(ErrorCondition.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/FlowTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/FlowTypeCodecTest.java
new file mode 100644
index 0000000..1902ae8
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/FlowTypeCodecTest.java
@@ -0,0 +1,473 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.FlowTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.FlowTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Flow;
+import org.junit.jupiter.api.Test;
+
+public class FlowTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Flow.class, new FlowTypeDecoder().getTypeClass());
+        assertEquals(Flow.class, new FlowTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Flow.DESCRIPTOR_CODE, new FlowTypeDecoder().getDescriptorCode());
+        assertEquals(Flow.DESCRIPTOR_CODE, new FlowTypeEncoder().getDescriptorCode());
+        assertEquals(Flow.DESCRIPTOR_SYMBOL, new FlowTypeDecoder().getDescriptorSymbol());
+        assertEquals(Flow.DESCRIPTOR_SYMBOL, new FlowTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testCannotEncodeEmptyPerformative() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Flow input = new Flow();
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+            fail("Cannot omit required fields.");
+        } catch (EncodeException encEx) {
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecode() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomeNextIncomingId = random.nextInt();
+        final int randomeNextOutgoingId = random.nextInt();
+        final int randomeNextIncomingWindow = random.nextInt();
+        final int randomeNextOutgoingWindow = random.nextInt();
+        final int randomeHandle = random.nextInt();
+        final int randomLinkCredit = random.nextInt();
+        final int randomeAvailable = random.nextInt();
+        final int randomeDeliveryCount = random.nextInt();
+
+        Flow input = new Flow();
+
+        input.setNextIncomingId(randomeNextIncomingId);
+        input.setIncomingWindow(randomeNextIncomingWindow);
+        input.setNextOutgoingId(randomeNextOutgoingId);
+        input.setOutgoingWindow(randomeNextOutgoingWindow);
+        input.setHandle(randomeHandle);
+        input.setDeliveryCount(randomeDeliveryCount);
+        input.setLinkCredit(randomLinkCredit);
+        input.setAvailable(randomeAvailable);
+        input.setDrain(true);
+        input.setEcho(true);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Flow result;
+        if (fromStream) {
+            result = (Flow) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Flow) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Integer.toUnsignedLong(randomeNextIncomingId), result.getNextIncomingId());
+        assertEquals(Integer.toUnsignedLong(randomeNextIncomingWindow), result.getIncomingWindow());
+        assertEquals(Integer.toUnsignedLong(randomeNextOutgoingId), result.getNextOutgoingId());
+        assertEquals(Integer.toUnsignedLong(randomeNextOutgoingWindow), result.getOutgoingWindow());
+        assertEquals(Integer.toUnsignedLong(randomeHandle), result.getHandle());
+        assertEquals(Integer.toUnsignedLong(randomeDeliveryCount), result.getDeliveryCount());
+        assertEquals(Integer.toUnsignedLong(randomLinkCredit), result.getLinkCredit());
+        assertEquals(Integer.toUnsignedLong(randomeAvailable), result.getAvailable());
+        assertTrue(input.getDrain());
+        assertTrue(input.getEcho());
+        assertNull(input.getProperties());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        testSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        testSkipValue(true);
+    }
+
+    private void testSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Flow flow = new Flow();
+
+        flow.setNextIncomingId(1);
+        flow.setIncomingWindow(2);
+        flow.setNextOutgoingId(3);
+        flow.setOutgoingWindow(4);
+        flow.setHandle(UnsignedInteger.valueOf(10).longValue());
+        flow.setDeliveryCount(5);
+        flow.setLinkCredit(6);
+        flow.setAvailable(7);
+        flow.setDrain(false);
+        flow.setEcho(false);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, flow);
+        }
+
+        flow.setNextIncomingId(10);
+        flow.setIncomingWindow(20);
+        flow.setNextOutgoingId(30);
+        flow.setOutgoingWindow(40);
+        flow.setHandle(UnsignedInteger.MAX_VALUE.longValue());
+        flow.setDeliveryCount(50);
+        flow.setLinkCredit(60);
+        flow.setAvailable(70);
+        flow.setDrain(true);
+        flow.setEcho(true);
+
+        encoder.writeObject(buffer, encoderState, flow);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Flow.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Flow.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Flow);
+
+        Flow value = (Flow) result;
+        assertEquals(10, value.getNextIncomingId());
+        assertEquals(20, value.getIncomingWindow());
+        assertEquals(30, value.getNextOutgoingId());
+        assertEquals(40, value.getOutgoingWindow());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), value.getHandle());
+        assertEquals(50, value.getDeliveryCount());
+        assertEquals(60, value.getLinkCredit());
+        assertEquals(70, value.getAvailable());
+        assertTrue(value.getDrain());
+        assertTrue(value.getEcho());
+        assertNull(value.getProperties());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Flow.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Flow.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Flow.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Flow.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        testEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        testEncodeDecodeArray(true);
+    }
+
+    private void testEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Flow[] array = new Flow[3];
+
+        array[0] = new Flow();
+        array[1] = new Flow();
+        array[2] = new Flow();
+
+        array[0].setHandle(0).setLinkCredit(0).setDeliveryCount(1).setIncomingWindow(1024).setNextOutgoingId(1).setOutgoingWindow(128);
+        array[1].setHandle(1).setLinkCredit(1).setDeliveryCount(1).setIncomingWindow(2048).setNextOutgoingId(2).setOutgoingWindow(256);
+        array[2].setHandle(2).setLinkCredit(2).setDeliveryCount(1).setIncomingWindow(4096).setNextOutgoingId(3).setOutgoingWindow(512);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Flow.class, result.getClass().getComponentType());
+
+        Flow[] resultArray = (Flow[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Flow);
+            assertEquals(array[i].getHandle(), resultArray[i].getHandle());
+            assertEquals(array[i].getLinkCredit(), resultArray[i].getLinkCredit());
+            assertEquals(array[i].getDeliveryCount(), resultArray[i].getDeliveryCount());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Flow.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Flow.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/OpenTypeCodecTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/OpenTypeCodecTest.java
new file mode 100644
index 0000000..3c1037c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/OpenTypeCodecTest.java
@@ -0,0 +1,878 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.OpenTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.OpenTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedShort;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.apache.qpid.protonj2.types.transport.Performative;
+import org.junit.jupiter.api.Test;
+
+public class OpenTypeCodecTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Open.class, new OpenTypeDecoder().getTypeClass());
+        assertEquals(Open.class, new OpenTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Open.DESCRIPTOR_CODE, new OpenTypeDecoder().getDescriptorCode());
+        assertEquals(Open.DESCRIPTOR_CODE, new OpenTypeEncoder().getDescriptorCode());
+        assertEquals(Open.DESCRIPTOR_SYMBOL, new OpenTypeDecoder().getDescriptorSymbol());
+        assertEquals(Open.DESCRIPTOR_SYMBOL, new OpenTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testEncodeAndDecode() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomChannelMax = random.nextInt(65535);
+        final int randomMaxFrameSize = random.nextInt();
+        final int randomIdleTimeout = random.nextInt();
+
+        Open input = new Open();
+
+        input.setContainerId("test");
+        input.setHostname("localhost");
+        input.setChannelMax(randomChannelMax);
+        input.setMaxFrameSize(randomMaxFrameSize);
+        input.setIdleTimeout(randomIdleTimeout);
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals("test", result.getContainerId());
+        assertEquals("localhost", result.getHostname());
+        assertEquals(randomChannelMax, result.getChannelMax());
+        assertEquals(Integer.toUnsignedLong(randomMaxFrameSize), result.getMaxFrameSize());
+        assertEquals(Integer.toUnsignedLong(randomIdleTimeout), result.getIdleTimeout());
+        assertArrayEquals(offeredCapabilities, result.getOfferedCapabilities());
+        assertArrayEquals(desiredCapabilities, result.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testOpenEncodesDefaultMaxFrameSizeWhenSet() throws IOException {
+        doTestOpenEncodesDefaultMaxFrameSizeWhenSet(false);
+    }
+
+    @Test
+    public void testOpenEncodesDefaultMaxFrameSizeWhenSetFromStream() throws IOException {
+        doTestOpenEncodesDefaultMaxFrameSizeWhenSet(true);
+    }
+
+    private void doTestOpenEncodesDefaultMaxFrameSizeWhenSet(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final Open input = new Open();
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open resultWithDefault = (Open) decoder.readObject(buffer, decoderState);
+
+        assertFalse(resultWithDefault.hasMaxFrameSize());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), resultWithDefault.getMaxFrameSize());
+
+        input.setMaxFrameSize(UnsignedInteger.MAX_VALUE.longValue());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.hasMaxFrameSize());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), result.getMaxFrameSize());
+    }
+
+    @Test
+    public void testOpenEncodesDefaultIdleTimeoutWhenSet() throws IOException {
+        doTestOpenEncodesDefaultIdleTimeoutWhenSet(false);
+    }
+
+    @Test
+    public void testOpenEncodesDefaultIdleTimeoutWhenSetFromStream() throws IOException {
+        doTestOpenEncodesDefaultIdleTimeoutWhenSet(true);
+    }
+
+    private void doTestOpenEncodesDefaultIdleTimeoutWhenSet(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final Open input = new Open();
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open resultWithDefault = (Open) decoder.readObject(buffer, decoderState);
+
+        assertFalse(resultWithDefault.hasIdleTimeout());
+        assertEquals(UnsignedInteger.ZERO.longValue(), resultWithDefault.getIdleTimeout());
+
+        input.setIdleTimeout(UnsignedInteger.ZERO.longValue());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.hasIdleTimeout());
+        assertEquals(UnsignedInteger.ZERO.longValue(), result.getIdleTimeout());
+    }
+
+    @Test
+    public void testOpenEncodesDefaultChannelMaxWhenSet() throws IOException {
+        doTestOpenEncodesDefaultChannelMaxWhenSet(false);
+    }
+
+    @Test
+    public void testOpenEncodesDefaultChannelMaxWhenSetFromStream() throws IOException {
+        doTestOpenEncodesDefaultChannelMaxWhenSet(true);
+    }
+
+    private void doTestOpenEncodesDefaultChannelMaxWhenSet(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        final Open input = new Open();
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open resultWithDefault = (Open) decoder.readObject(buffer, decoderState);
+
+        assertFalse(resultWithDefault.hasChannelMax());
+        assertEquals(UnsignedShort.MAX_VALUE.intValue(), resultWithDefault.getChannelMax());
+
+        input.setChannelMax(UnsignedShort.MAX_VALUE.intValue());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.hasChannelMax());
+        assertEquals(UnsignedShort.MAX_VALUE.intValue(), result.getChannelMax());
+    }
+
+    @Test
+    public void testEncodeAndDecodeOpenWithMaxMaxFrameSize() throws IOException {
+        doTestEncodeAndDecodeOpenWithMaxMaxFrameSize(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeOpenWithMaxMaxFrameSizeFromStream() throws IOException {
+        doTestEncodeAndDecodeOpenWithMaxMaxFrameSize(true);
+    }
+
+    private void doTestEncodeAndDecodeOpenWithMaxMaxFrameSize(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Open input = new Open();
+
+        input.setContainerId("test");
+        input.setHostname("localhost");
+        input.setChannelMax(UnsignedShort.MAX_VALUE.intValue());
+        input.setMaxFrameSize(UnsignedInteger.MAX_VALUE.longValue());
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals("test", result.getContainerId());
+        assertEquals("localhost", result.getHostname());
+        assertEquals(UnsignedShort.MAX_VALUE.intValue(), result.getChannelMax());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), result.getMaxFrameSize());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Open close = new Open();
+
+        close.setContainerId("skip");
+        close.setHostname("google");
+        close.setChannelMax(UnsignedShort.valueOf(256).intValue());
+        close.setMaxFrameSize(UnsignedInteger.ZERO.longValue());
+        close.setIdleTimeout(UnsignedInteger.ONE.longValue());
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, close);
+        }
+
+        close.setContainerId("test");
+        close.setHostname("localhost");
+        close.setChannelMax(UnsignedShort.valueOf(512).intValue());
+        close.setMaxFrameSize(UnsignedInteger.ONE.longValue());
+        close.setIdleTimeout(UnsignedInteger.ZERO.longValue());
+
+        encoder.writeObject(buffer, encoderState, close);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Open.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Open.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Open);
+
+        Open value = result;
+        assertEquals("test", value.getContainerId());
+        assertEquals("localhost", value.getHostname());
+        assertEquals(UnsignedShort.valueOf(512).intValue(), value.getChannelMax());
+        assertEquals(UnsignedInteger.ONE.longValue(), value.getMaxFrameSize());
+        assertEquals(UnsignedInteger.ZERO.longValue(), value.getIdleTimeout());
+        assertNull(value.getOfferedCapabilities());
+        assertNull(value.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testEncodeUsingNewCodecAndDecodeWithLegacyCodec() throws Exception {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+
+        Open input = new Open();
+
+        input.setContainerId("test");
+        input.setHostname("localhost");
+        input.setMaxFrameSize(UnsignedInteger.ONE.longValue());
+        input.setIdleTimeout(UnsignedInteger.ZERO.longValue());
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+
+        encoder.writeObject(buffer, encoderState, input);
+        Object decoded = legacyCodec.decodeLegacyType(buffer);
+        assertTrue(decoded instanceof Open);
+        final Open result = (Open) decoded;
+
+        assertNotNull(result);
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodec() throws Exception {
+        doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(false);
+    }
+
+    @Test
+    public void testEncodeUsingLegacyCodecAndDecodeWithNewCodecFromStream() throws Exception {
+        doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(true);
+    }
+
+    public void doTestEncodeUsingLegacyCodecAndDecodeWithNewCodec(boolean fromStream) throws Exception {
+        Symbol[] offeredCapabilities = new Symbol[] {Symbol.valueOf("Cap-1"), Symbol.valueOf("Cap-2")};
+        Symbol[] desiredCapabilities = new Symbol[] {Symbol.valueOf("Cap-3"), Symbol.valueOf("Cap-4")};
+
+        Open input = new Open();
+
+        input.setContainerId("test");
+        input.setHostname("localhost");
+        input.setMaxFrameSize(UnsignedInteger.MAX_VALUE.longValue());
+        input.setIdleTimeout(UnsignedInteger.ZERO.longValue());
+        input.setOfferedCapabilities(offeredCapabilities);
+        input.setDesiredCapabilities(desiredCapabilities);
+
+        ProtonBuffer buffer = legacyCodec.encodeUsingLegacyEncoder(input);
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        assertNotNull(buffer);
+
+        final Open result;
+        if (fromStream) {
+            result = (Open) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Open) decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+
+        assertTypesEqual(input, result);
+    }
+
+    @Test
+    public void testToStringWhenEmptyNoNPE() {
+        Open open = new Open();
+        assertNotNull(open.toString());
+    }
+
+    @Test
+    public void testPerformativeType() {
+        Open open = new Open();
+        assertEquals(Performative.PerformativeType.OPEN, open.getPerformativeType());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Open open = new Open();
+
+        // Open defaults to an empty string container ID so not empty
+        assertFalse(open.isEmpty());
+        open.setMaxFrameSize(1024);
+        assertFalse(open.isEmpty());
+    }
+
+    @Test
+    public void testContainerId() {
+        Open open = new Open();
+
+        // Open defaults to an empty string container ID
+        assertTrue(open.hasContainerId());
+        assertEquals("", open.getContainerId());
+        open.setContainerId("test");
+        assertTrue(open.hasContainerId());
+        assertEquals("test", open.getContainerId());
+
+        try {
+            open.setContainerId(null);
+            fail("Should not be able to set a null container id for mandatory field.");
+        } catch (NullPointerException npe) {
+        }
+    }
+
+    @Test
+    public void testHostname() {
+        Open open = new Open();
+
+        assertFalse(open.hasHostname());
+        assertNull(open.getHostname());
+        open.setHostname("localhost");
+        assertTrue(open.hasHostname());
+        assertEquals("localhost", open.getHostname());
+        open.setHostname(null);
+        assertFalse(open.hasHostname());
+        assertNull(open.getHostname());
+    }
+
+    @Test
+    public void testMaxFrameSize() {
+        Open open = new Open();
+
+        assertFalse(open.hasMaxFrameSize());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), open.getMaxFrameSize());
+        open.setMaxFrameSize(1024);
+        assertTrue(open.hasMaxFrameSize());
+        assertEquals(1024, open.getMaxFrameSize());
+    }
+
+    @Test
+    public void testChannelMax() {
+        Open open = new Open();
+
+        assertFalse(open.hasChannelMax());
+        assertEquals(UnsignedShort.MAX_VALUE.longValue(), open.getChannelMax());
+        open.setChannelMax(1024);
+        assertTrue(open.hasChannelMax());
+        assertEquals(1024, open.getChannelMax());
+    }
+
+    @Test
+    public void testIdleTimeout() {
+        Open open = new Open();
+
+        assertFalse(open.hasIdleTimeout());
+        assertEquals(0, open.getIdleTimeout());
+        open.setIdleTimeout(1024);
+        assertTrue(open.hasIdleTimeout());
+        assertEquals(1024, open.getIdleTimeout());
+    }
+
+    @Test
+    public void testOutgoingLocales() {
+        Open open = new Open();
+
+        assertFalse(open.hasOutgoingLocales());
+        assertNull(open.getOutgoingLocales());
+        open.setOutgoingLocales(Symbol.valueOf("test"));
+        assertTrue(open.hasOutgoingLocales());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("test") }, open.getOutgoingLocales());
+        open.setOutgoingLocales((Symbol[]) null);
+        assertFalse(open.hasDesiredCapabilites());
+        assertNull(open.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testIncomingLocales() {
+        Open open = new Open();
+
+        assertFalse(open.hasIncomingLocales());
+        assertNull(open.getIncomingLocales());
+        open.setIncomingLocales(Symbol.valueOf("test"));
+        assertTrue(open.hasIncomingLocales());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("test") }, open.getIncomingLocales());
+        open.setIncomingLocales((Symbol[]) null);
+        assertFalse(open.hasDesiredCapabilites());
+        assertNull(open.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testOfferedCapabilities() {
+        Open open = new Open();
+
+        assertFalse(open.hasOfferedCapabilites());
+        assertNull(open.getOfferedCapabilities());
+        open.setOfferedCapabilities(Symbol.valueOf("test"));
+        assertTrue(open.hasOfferedCapabilites());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("test") }, open.getOfferedCapabilities());
+        open.setOfferedCapabilities((Symbol[]) null);
+        assertFalse(open.hasDesiredCapabilites());
+        assertNull(open.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testDesiredCapabilities() {
+        Open open = new Open();
+
+        assertFalse(open.hasDesiredCapabilites());
+        assertNull(open.getDesiredCapabilities());
+        open.setDesiredCapabilities(Symbol.valueOf("test"));
+        assertTrue(open.hasDesiredCapabilites());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("test") }, open.getDesiredCapabilities());
+        open.setDesiredCapabilities((Symbol[]) null);
+        assertFalse(open.hasDesiredCapabilites());
+        assertNull(open.getDesiredCapabilities());
+    }
+
+    @Test
+    public void testProperties() {
+        Open open = new Open();
+
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), Boolean.FALSE);
+
+        assertFalse(open.hasProperties());
+        assertNull(open.getProperties());
+        open.setProperties(properties);
+        assertTrue(open.hasProperties());
+        assertEquals(properties, open.getProperties());
+        open.setProperties(null);
+        assertFalse(open.hasProperties());
+        assertNull(open.getProperties());
+    }
+
+    @Test
+    public void testCopy() {
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), Boolean.FALSE);
+
+        Symbol outgoingLocale = Symbol.valueOf("outgoing");
+        Symbol incomingLocale = Symbol.valueOf("incoming");
+        Symbol offeredCapability = Symbol.valueOf("offered");
+        Symbol desiredCapability = Symbol.valueOf("desired");
+
+        Open open = new Open();
+
+        open.setContainerId("test");
+        open.setHostname("localhost");
+        open.setMaxFrameSize(1024);
+        open.setChannelMax(64);
+        open.setIdleTimeout(360000);
+        open.setOutgoingLocales(outgoingLocale);
+        open.setIncomingLocales(incomingLocale);
+        open.setOfferedCapabilities(offeredCapability);
+        open.setDesiredCapabilities(desiredCapability);
+        open.setProperties(properties);
+
+        Open copy = open.copy();
+
+        assertNotNull(copy);
+
+        assertEquals("test", open.getContainerId());
+        assertEquals("localhost", open.getHostname());
+        assertEquals(1024, open.getMaxFrameSize());
+        assertEquals(64, open.getChannelMax());
+        assertEquals(360000, open.getIdleTimeout());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("outgoing") }, open.getOutgoingLocales());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("incoming") }, open.getIncomingLocales());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("offered") }, open.getOfferedCapabilities());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("desired") }, open.getDesiredCapabilities());
+        assertEquals(properties, open.getProperties());
+
+        open.setOutgoingLocales((Symbol[]) null);
+        open.setIncomingLocales((Symbol[]) null);
+        open.setOfferedCapabilities((Symbol[]) null);
+        open.setDesiredCapabilities((Symbol[]) null);
+        open.setProperties(null);
+
+        copy = open.copy();
+
+        assertNotNull(copy);
+
+        assertEquals("test", open.getContainerId());
+        assertEquals("localhost", open.getHostname());
+        assertEquals(1024, open.getMaxFrameSize());
+        assertEquals(64, open.getChannelMax());
+        assertEquals(360000, open.getIdleTimeout());
+        assertNull(open.getOutgoingLocales());
+        assertNull(open.getIncomingLocales());
+        assertNull(open.getOfferedCapabilities());
+        assertNull(open.getDesiredCapabilities());
+        assertNull(open.getProperties());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Open.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Open.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Open.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Open.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Open[] array = new Open[3];
+
+        array[0] = new Open();
+        array[1] = new Open();
+        array[2] = new Open();
+
+        array[0].setHostname("1").setIdleTimeout(1).setMaxFrameSize(1);
+        array[1].setHostname("2").setIdleTimeout(2).setMaxFrameSize(2);
+        array[2].setHostname("3").setIdleTimeout(3).setMaxFrameSize(3);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Open.class, result.getClass().getComponentType());
+
+        Open[] resultArray = (Open[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Open);
+            assertEquals(array[i].getHostname(), resultArray[i].getHostname());
+            assertEquals(array[i].getIdleTimeout(), resultArray[i].getIdleTimeout());
+            assertEquals(array[i].getMaxFrameSize(), resultArray[i].getMaxFrameSize());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Open.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Open.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/TransferTypeCodeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/TransferTypeCodeTest.java
new file mode 100644
index 0000000..651fcdc
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/transport/TransferTypeCodeTest.java
@@ -0,0 +1,455 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferInputStream;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecTestSupport;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.StreamTypeDecoder;
+import org.apache.qpid.protonj2.codec.TypeDecoder;
+import org.apache.qpid.protonj2.codec.decoders.transport.TransferTypeDecoder;
+import org.apache.qpid.protonj2.codec.encoders.transport.TransferTypeEncoder;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+import org.junit.jupiter.api.Test;
+
+public class TransferTypeCodeTest extends CodecTestSupport {
+
+    @Test
+    public void testTypeClassReturnsCorrectType() throws IOException {
+        assertEquals(Transfer.class, new TransferTypeDecoder().getTypeClass());
+        assertEquals(Transfer.class, new TransferTypeEncoder().getTypeClass());
+    }
+
+    @Test
+    public void testDescriptors() throws IOException {
+        assertEquals(Transfer.DESCRIPTOR_CODE, new TransferTypeDecoder().getDescriptorCode());
+        assertEquals(Transfer.DESCRIPTOR_CODE, new TransferTypeEncoder().getDescriptorCode());
+        assertEquals(Transfer.DESCRIPTOR_SYMBOL, new TransferTypeDecoder().getDescriptorSymbol());
+        assertEquals(Transfer.DESCRIPTOR_SYMBOL, new TransferTypeEncoder().getDescriptorSymbol());
+    }
+
+    @Test
+    public void testCannotEncodeEmptyPerformative() throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+
+        Transfer input = new Transfer();
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+            fail("Cannot omit required fields.");
+        } catch (EncodeException encEx) {
+        }
+    }
+
+    @Test
+    public void testEncodeAndDecode() throws IOException {
+        doTestEncodeAndDecode(false);
+    }
+
+    @Test
+    public void testEncodeAndDecodeFromStream() throws IOException {
+        doTestEncodeAndDecode(true);
+    }
+
+    private void doTestEncodeAndDecode(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ProtonBuffer tag = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2});
+
+        final Random random = new Random();
+        random.setSeed(System.nanoTime());
+
+        final int randomHandle = random.nextInt();
+        final int randomDeliveryId = random.nextInt();
+        final int randomMessageFormat = random.nextInt();
+
+        Transfer input = new Transfer();
+
+        input.setHandle(randomHandle);
+        input.setDeliveryId(randomDeliveryId);
+        input.setDeliveryTag(tag);
+        input.setMessageFormat(randomMessageFormat);
+        input.setSettled(false);
+        input.setBatchable(false);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        final Transfer result;
+        if (fromStream) {
+            result = (Transfer) streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = (Transfer) decoder.readObject(buffer, decoderState);
+        }
+
+        assertEquals(Integer.toUnsignedLong(randomHandle), result.getHandle());
+        assertEquals(Integer.toUnsignedLong(randomDeliveryId), result.getDeliveryId());
+        assertEquals(tag, result.getDeliveryTag().tagBuffer());
+        assertEquals(Integer.toUnsignedLong(randomMessageFormat), result.getMessageFormat());
+        assertFalse(result.getSettled());
+        assertFalse(result.getBatchable());
+    }
+
+    @Test
+    public void testSkipValue() throws IOException {
+        doTestSkipValue(false);
+    }
+
+    @Test
+    public void testSkipValueFromStream() throws IOException {
+        doTestSkipValue(true);
+    }
+
+    private void doTestSkipValue(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        ProtonBuffer tag = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2});
+
+        Transfer input = new Transfer();
+
+        input.setHandle(UnsignedInteger.valueOf(2).longValue());
+        input.setDeliveryId(100);
+        input.setDeliveryTag(tag);
+        input.setMessageFormat(1);
+        input.setSettled(true);
+        input.setBatchable(true);
+        input.setResume(true);
+
+        for (int i = 0; i < 10; ++i) {
+            encoder.writeObject(buffer, encoderState, input);
+        }
+
+        input.setHandle(UnsignedInteger.MAX_VALUE.longValue());
+        input.setDeliveryId(10);
+        input.setDeliveryTag(tag);
+        input.setMessageFormat(0);
+        input.setSettled(false);
+        input.setState(null);
+        input.setBatchable(false);
+
+        encoder.writeObject(buffer, encoderState, input);
+
+        for (int i = 0; i < 10; ++i) {
+            if (fromStream) {
+                StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+                assertEquals(Transfer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(stream, streamDecoderState);
+            } else {
+                TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+                assertEquals(Transfer.class, typeDecoder.getTypeClass());
+                typeDecoder.skipValue(buffer, decoderState);
+            }
+        }
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertNotNull(result);
+        assertTrue(result instanceof Transfer);
+
+        Transfer value = (Transfer) result;
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), value.getHandle());
+        assertEquals(10, value.getDeliveryId());
+        assertEquals(tag, value.getDeliveryTag().tagBuffer());
+        assertEquals(0, value.getMessageFormat());
+        assertFalse(value.getSettled());
+        assertFalse(value.getBatchable());
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8Type() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap32TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testSkipValueWithInvalidMap8TypeFromStream() throws IOException {
+        doTestSkipValueWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestSkipValueWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Transfer.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            StreamTypeDecoder<?> typeDecoder = streamDecoder.readNextTypeDecoder(stream, streamDecoderState);
+            assertEquals(Transfer.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(stream, streamDecoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            TypeDecoder<?> typeDecoder = decoder.readNextTypeDecoder(buffer, decoderState);
+            assertEquals(Transfer.class, typeDecoder.getTypeClass());
+
+            try {
+                typeDecoder.skipValue(buffer, decoderState);
+                fail("Should not be able to skip type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, false);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8Type() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, false);
+    }
+
+    @Test
+    public void testDecodedWithInvalidMap32TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP32, true);
+    }
+
+    @Test
+    public void testDecodeWithInvalidMap8TypeFromStream() throws IOException {
+        doTestDecodeWithInvalidMapType(EncodingCodes.MAP8, true);
+    }
+
+    private void doTestDecodeWithInvalidMapType(byte mapType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Transfer.DESCRIPTOR_CODE.byteValue());
+        if (mapType == EncodingCodes.MAP32) {
+            buffer.writeByte(EncodingCodes.MAP32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.MAP8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid encoding");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeArray() throws IOException {
+        doTestEncodeDecodeArray(false);
+    }
+
+    @Test
+    public void testEncodeDecodeArrayFromStream() throws IOException {
+        doTestEncodeDecodeArray(true);
+    }
+
+    private void doTestEncodeDecodeArray(boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        Transfer[] array = new Transfer[3];
+
+        array[0] = new Transfer();
+        array[1] = new Transfer();
+        array[2] = new Transfer();
+
+        ProtonBuffer tag1 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0});
+        ProtonBuffer tag2 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {1});
+        ProtonBuffer tag3 = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {2});
+
+        array[0].setHandle(0).setDeliveryTag(tag1);
+        array[1].setHandle(1).setDeliveryTag(tag2);
+        array[2].setHandle(2).setDeliveryTag(tag3);
+
+        encoder.writeObject(buffer, encoderState, array);
+
+        final Object result;
+        if (fromStream) {
+            result = streamDecoder.readObject(stream, streamDecoderState);
+        } else {
+            result = decoder.readObject(buffer, decoderState);
+        }
+
+        assertTrue(result.getClass().isArray());
+        assertEquals(Transfer.class, result.getClass().getComponentType());
+
+        Transfer[] resultArray = (Transfer[]) result;
+
+        for (int i = 0; i < resultArray.length; ++i) {
+            assertNotNull(resultArray[i]);
+            assertTrue(resultArray[i] instanceof Transfer);
+            assertEquals(array[i].getHandle(), resultArray[i].getHandle());
+            assertEquals(array[i].getDeliveryTag(), resultArray[i].getDeliveryTag());
+        }
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList0FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST0, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithNotEnoughListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithNotEnoughListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithNotEnoughListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Transfer.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt((byte) 0);  // Size
+            buffer.writeInt((byte) 0);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 0);  // Size
+            buffer.writeByte((byte) 0);  // Count
+        } else {
+            buffer.writeByte(EncodingCodes.LIST0);
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, false);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList8FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST8, true);
+    }
+
+    @Test
+    public void testDecodeWithToManyListEntriesList32FromStream() throws IOException {
+        doTestDecodeWithToManyListEntriesList32(EncodingCodes.LIST32, true);
+    }
+
+    private void doTestDecodeWithToManyListEntriesList32(byte listType, boolean fromStream) throws IOException {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate();
+        InputStream stream = new ProtonBufferInputStream(buffer);
+
+        buffer.writeByte((byte) 0); // Described Type Indicator
+        buffer.writeByte(EncodingCodes.SMALLULONG);
+        buffer.writeByte(Transfer.DESCRIPTOR_CODE.byteValue());
+        if (listType == EncodingCodes.LIST32) {
+            buffer.writeByte(EncodingCodes.LIST32);
+            buffer.writeInt(128);  // Size
+            buffer.writeInt(127);  // Count
+        } else if (listType == EncodingCodes.LIST8) {
+            buffer.writeByte(EncodingCodes.LIST8);
+            buffer.writeByte((byte) 128);  // Size
+            buffer.writeByte((byte) 127);  // Count
+        }
+
+        if (fromStream) {
+            try {
+                streamDecoder.readObject(stream, streamDecoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        } else {
+            try {
+                decoder.readObject(buffer, decoderState);
+                fail("Should not decode type with invalid min entries");
+            } catch (DecodeException ex) {}
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalType.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalType.java
new file mode 100644
index 0000000..76872eb
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalType.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.util;
+
+import org.apache.qpid.protonj2.types.DescribedType;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+/**
+ * A Described Type wrapper for JMS no local option for MessageConsumer.
+ */
+public class NoLocalType implements DescribedType {
+
+    public static final NoLocalType NO_LOCAL = new NoLocalType();
+
+    public static final UnsignedLong DESCRIPTOR_CODE = UnsignedLong.valueOf(0x0000468C00000003L);
+    public static final Symbol DESCRIPTOR_SYMBOL = Symbol.valueOf("apache.org:no-local-filter:list");
+
+    private final String noLocal;
+
+    public NoLocalType() {
+        this.noLocal = "NoLocalFilter{}";
+    }
+
+    @Override
+    public Object getDescriptor() {
+        return DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public String getDescribed() {
+        return this.noLocal;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalTypeDecoder.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalTypeDecoder.java
new file mode 100644
index 0000000..8845836
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalTypeDecoder.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.util;
+
+import java.io.InputStream;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.DecodeException;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.StreamDecoderState;
+import org.apache.qpid.protonj2.codec.decoders.AbstractDescribedTypeDecoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public class NoLocalTypeDecoder extends AbstractDescribedTypeDecoder<NoLocalType> {
+
+    @Override
+    public Class<NoLocalType> getTypeClass() {
+        return NoLocalType.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return NoLocalType.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return NoLocalType.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public NoLocalType readValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        state.getDecoder().readString(buffer, state);
+
+        return NoLocalType.NO_LOCAL;
+    }
+
+    @Override
+    public NoLocalType readValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        state.getDecoder().readString(stream, state);
+
+        return NoLocalType.NO_LOCAL;
+    }
+
+    @Override
+    public NoLocalType[] readArrayElements(ProtonBuffer buffer, DecoderState state, int count) throws DecodeException {
+        NoLocalType[] array = new NoLocalType[count];
+        for (int i = 0; i < count; ++i) {
+            array[i] = readValue(buffer, state);
+        }
+
+        return array;
+    }
+
+    @Override
+    public NoLocalType[] readArrayElements(InputStream stream, StreamDecoderState state, int count) throws DecodeException {
+        NoLocalType[] array = new NoLocalType[count];
+        for (int i = 0; i < count; ++i) {
+            array[i] = readValue(stream, state);
+        }
+
+        return array;
+    }
+
+    @Override
+    public void skipValue(ProtonBuffer buffer, DecoderState state) throws DecodeException {
+        state.getDecoder().readNextTypeDecoder(buffer, state).skipValue(buffer, state);
+    }
+
+    @Override
+    public void skipValue(InputStream stream, StreamDecoderState state) throws DecodeException {
+        state.getDecoder().readNextTypeDecoder(stream, state).skipValue(stream, state);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalTypeEncoder.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalTypeEncoder.java
new file mode 100644
index 0000000..f239bd5
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/NoLocalTypeEncoder.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.util;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.codec.EncodingCodes;
+import org.apache.qpid.protonj2.codec.TypeEncoder;
+import org.apache.qpid.protonj2.codec.encoders.AbstractDescribedTypeEncoder;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+
+public class NoLocalTypeEncoder extends AbstractDescribedTypeEncoder<NoLocalType> {
+
+    @Override
+    public Class<NoLocalType> getTypeClass() {
+        return NoLocalType.class;
+    }
+
+    @Override
+    public UnsignedLong getDescriptorCode() {
+        return NoLocalType.DESCRIPTOR_CODE;
+    }
+
+    @Override
+    public Symbol getDescriptorSymbol() {
+        return NoLocalType.DESCRIPTOR_SYMBOL;
+    }
+
+    @Override
+    public void writeType(ProtonBuffer buffer, EncoderState state, NoLocalType value) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+        state.getEncoder().writeString(buffer, state, value.getDescribed());
+    }
+
+    @Override
+    public void writeArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        // Write the Array Type encoding code, we don't optimize here.
+        buffer.writeByte(EncodingCodes.ARRAY32);
+
+        int startIndex = buffer.getWriteIndex();
+
+        // Reserve space for the size and write the count of list elements.
+        buffer.writeInt(0);
+        buffer.writeInt(values.length);
+
+        writeRawArray(buffer, state, values);
+
+        // Move back and write the size
+        int endIndex = buffer.getWriteIndex();
+        long writeSize = endIndex - startIndex - Integer.BYTES;
+
+        if (writeSize > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cannot encode given array, encoded size to large: " + writeSize);
+        }
+
+        buffer.setInt(startIndex, (int) writeSize);
+    }
+
+    @Override
+    public void writeRawArray(ProtonBuffer buffer, EncoderState state, Object[] values) {
+        buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
+        state.getEncoder().writeUnsignedLong(buffer, state, getDescriptorCode());
+
+        Object[] elements = new Object[values.length];
+
+        for (int i = 0; i < values.length; ++i) {
+            NoLocalType value = (NoLocalType) values[i];
+            elements[i] = value.getDescribed();
+        }
+
+        TypeEncoder<?> entryEncoder = state.getEncoder().getTypeEncoder(String.class);
+        entryEncoder.writeArray(buffer, state, elements);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/SimplePojo.java b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/SimplePojo.java
new file mode 100644
index 0000000..50f25a2
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/codec/util/SimplePojo.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.codec.util;
+
+import java.io.Serializable;
+
+public class SimplePojo implements Serializable {
+
+    private static final long serialVersionUID = 3258560248864895099L;
+
+    private Object payload;
+
+    public SimplePojo() {
+    }
+
+    public SimplePojo(Object payload) {
+        this.payload = payload;
+    }
+
+    public Object getPayload() {
+        return payload;
+    }
+
+    public void setPayload(Object payload) {
+        this.payload = payload;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((payload == null) ? 0 : payload.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        SimplePojo other = (SimplePojo) obj;
+        if (payload == null) {
+            if (other.payload != null) {
+                return false;
+            }
+        } else if (!payload.equals(other.payload)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/OutgoingAMQPEnvelopeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/OutgoingAMQPEnvelopeTest.java
new file mode 100644
index 0000000..326c2c5
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/OutgoingAMQPEnvelopeTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.apache.qpid.protonj2.types.transport.Performative.PerformativeHandler;
+import org.junit.jupiter.api.Test;
+
+class OutgoingAMQPEnvelopeTest {
+
+    @Test
+    void testCreateFromDefaultNoPool() {
+        OutgoingAMQPEnvelope envelope = new OutgoingAMQPEnvelope();
+
+        assertThrows(IllegalArgumentException.class, () -> envelope.handlePayloadToLarge());
+
+        envelope.setPayloadToLargeHandler(null);
+
+        assertThrows(IllegalArgumentException.class, () -> envelope.handlePayloadToLarge());
+
+        envelope.setPayloadToLargeHandler((perf) -> {});
+
+        assertDoesNotThrow(() -> envelope.handlePayloadToLarge());
+
+        envelope.handleOutgoingFrameWriteComplete();
+        envelope.release();
+
+        assertNotNull(envelope.toString());
+    }
+
+    @Test
+    void testInvokeHandlerOnPerformative() {
+        OutgoingAMQPEnvelope envelope = new OutgoingAMQPEnvelope();
+        envelope.initialize(new Open(), 0, null);
+
+        final AtomicBoolean signal = new AtomicBoolean();
+
+        envelope.invoke(new PerformativeHandler<OutgoingAMQPEnvelope>() {
+
+            @Override
+            public void handleOpen(Open open, ProtonBuffer payload, int channel, OutgoingAMQPEnvelope context) {
+                signal.set(true);
+            }
+
+        }, envelope);
+
+        assertTrue(signal.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/ProtocolFramePoolTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/ProtocolFramePoolTest.java
new file mode 100644
index 0000000..8f06e20
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/ProtocolFramePoolTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+import org.junit.jupiter.api.Test;
+
+class ProtocolFramePoolTest {
+
+    @Test
+    void testCreateIncomingFramePool() {
+        AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.incomingEnvelopePool();
+
+        assertNotNull(pool);
+        assertEquals(AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE, pool.getMaxPoolSize());
+
+        IncomingAMQPEnvelope frame1 = pool.take(new Transfer(), 0, null);
+        assertNotNull(frame1);
+        IncomingAMQPEnvelope frame2 = pool.take(new Transfer(), 0, null);
+        assertNotNull(frame2);
+
+        assertNotSame(frame1, frame2);
+    }
+
+    @Test
+    void testCreateIncomingFramePoolWithConfiguredMaxSize() {
+        AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.incomingEnvelopePool(AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE + 10);
+
+        assertEquals(AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE + 10, pool.getMaxPoolSize());
+
+        IncomingAMQPEnvelope frame1 = pool.take(new Transfer(), 0, null);
+
+        frame1.release();
+
+        IncomingAMQPEnvelope frame2 = pool.take(new Transfer(), 0, null);
+
+        assertSame(frame1, frame2);
+    }
+
+    @Test
+    void testIncomingPoolRecyclesReleasedFrames() {
+        AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.incomingEnvelopePool();
+        IncomingAMQPEnvelope frame1 = pool.take(new Transfer(), 0, null);
+
+        frame1.release();
+
+        IncomingAMQPEnvelope frame2 = pool.take(new Transfer(), 0, null);
+
+        assertSame(frame1, frame2);
+    }
+
+    @Test
+    void testInomingPoolClearsReleasedFramePayloads() {
+        AMQPPerformativeEnvelopePool<IncomingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.incomingEnvelopePool();
+        IncomingAMQPEnvelope frame1 = pool.take(new Transfer(), 2, ProtonByteBufferAllocator.DEFAULT.allocate());
+
+        frame1.release();
+
+        assertNull(frame1.getBody());
+        assertNull(frame1.getPayload());
+        assertNotEquals(2, frame1.getChannel());
+    }
+
+    @Test
+    void testCreateOutgoingFramePool() {
+        AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.outgoingEnvelopePool();
+
+        assertNotNull(pool);
+        assertEquals(AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE, pool.getMaxPoolSize());
+
+        OutgoingAMQPEnvelope frame1 = pool.take(new Transfer(), 0, null);
+        assertNotNull(frame1);
+        OutgoingAMQPEnvelope frame2 = pool.take(new Transfer(), 0, null);
+        assertNotNull(frame2);
+
+        assertNotEquals(frame1, frame2);
+    }
+
+    @Test
+    void testCreateOutgoingFramePoolWithConfiguredMaxSize() {
+        AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.outgoingEnvelopePool(AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE + 10);
+
+        assertEquals(AMQPPerformativeEnvelopePool.DEFAULT_MAX_POOL_SIZE + 10, pool.getMaxPoolSize());
+
+        OutgoingAMQPEnvelope frame1 = pool.take(new Transfer(), 0, null);
+
+        frame1.release();
+
+        OutgoingAMQPEnvelope frame2 = pool.take(new Transfer(), 0, null);
+
+        assertSame(frame1, frame2);
+    }
+
+    @Test
+    void testOutgoingPoolRecyclesReleasedFrames() {
+        AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.outgoingEnvelopePool();
+        OutgoingAMQPEnvelope frame1 = pool.take(new Transfer(), 0, null);
+
+        frame1.release();
+
+        OutgoingAMQPEnvelope frame2 = pool.take(new Transfer(), 0, null);
+
+        assertSame(frame1, frame2);
+    }
+
+    @Test
+    void testOutgoingPoolClearsReleasedFramePayloads() {
+        AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> pool = AMQPPerformativeEnvelopePool.outgoingEnvelopePool();
+        OutgoingAMQPEnvelope frame1 = pool.take(new Transfer(), 2, ProtonByteBufferAllocator.DEFAULT.allocate());
+
+        frame1.release();
+
+        assertNull(frame1.getBody());
+        assertNull(frame1.getPayload());
+        assertNotEquals(2, frame1.getChannel());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonConnectionTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonConnectionTest.java
new file mode 100644
index 0000000..078d076
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonConnectionTest.java
@@ -0,0 +1,1892 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.qpid.protonj2.codec.EncodeException;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.ConnectionState;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.FrameEncodingException;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.test.driver.matchers.types.UnsignedIntegerMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.types.UnsignedShortMatcher;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests for behaviors of the ProtonConnection class
+ */
+@Timeout(20)
+public class ProtonConnectionTest extends ProtonEngineTestSupport {
+
+    @Test
+    public void testConnectionSyncStateAfterEngineStarted() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        connection.open();
+
+        assertEquals(connection, connection.getParent());
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().ofSender().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        engine.start();
+
+        sender.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNegotiateSendsAMQPHeader() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+
+        Connection connection = engine.start();
+
+        connection.negotiate();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNegotiateSendsAMQPHeaderAndFireRemoteHeaderEvent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+
+        Connection connection = engine.start();
+
+        connection.negotiate();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNegotiateSendsAMQPHeaderEnforcesNotNullEventHandler() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+
+        try {
+            connection.negotiate(null);
+            fail("Should not allow null event handler");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNegotiateDoesNotSendAMQPHeaderAfterOpen() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicInteger headerReceivedCallback = new AtomicInteger();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        connection.negotiate(amqpHeader -> headerReceivedCallback.incrementAndGet());
+        assertEquals(1, headerReceivedCallback.get());
+        connection.negotiate(amqpHeader -> headerReceivedCallback.incrementAndGet());
+        assertEquals(2, headerReceivedCallback.get());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionEmitsOpenAndCloseEvents() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean connectionLocalOpen = new AtomicBoolean();
+        final AtomicBoolean connectionLocalClose = new AtomicBoolean();
+        final AtomicBoolean connectionRemoteOpen = new AtomicBoolean();
+        final AtomicBoolean connectionRemoteClose = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.localOpenHandler(result -> connectionLocalOpen.set(true))
+                  .localCloseHandler(result -> connectionLocalClose.set(true))
+                  .openHandler(result -> connectionRemoteOpen.set(true))
+                  .closeHandler(result -> connectionRemoteClose.set(true));
+
+        connection.open();
+        connection.close();
+
+        assertTrue(connectionLocalOpen.get(), "Connection should have reported local open");
+        assertTrue(connectionLocalClose.get(), "Connection should have reported local close");
+        assertTrue(connectionRemoteOpen.get(), "Connection should have reported remote open");
+        assertTrue(connectionRemoteClose.get(), "Connection should have reported remote close");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionPopulatesRemoteData() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Symbol[] offeredCapabilities = new Symbol[] { Symbol.valueOf("one"), Symbol.valueOf("two") };
+        Symbol[] desiredCapabilities = new Symbol[] { Symbol.valueOf("three"), Symbol.valueOf("four") };
+
+        Map<String, Object> expectedProperties = new HashMap<>();
+        expectedProperties.put("test", "value");
+
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), "value");
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("test")
+                                   .withHostname("localhost")
+                                   .withIdleTimeOut(60000)
+                                   .withOfferedCapabilities(new String[] { "one", "two" })
+                                   .withDesiredCapabilities(new String[] { "three", "four" })
+                                   .withProperties(expectedProperties);
+        peer.expectClose();
+
+        Connection connection = engine.start().open();
+
+        assertEquals("test", connection.getRemoteContainerId());
+        assertEquals("localhost", connection.getRemoteHostname());
+        assertEquals(60000, connection.getRemoteIdleTimeout());
+
+        assertArrayEquals(offeredCapabilities, connection.getRemoteOfferedCapabilities());
+        assertArrayEquals(desiredCapabilities, connection.getRemoteDesiredCapabilities());
+        assertEquals(properties, connection.getRemoteProperties());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenAndCloseConnectionWithNullSetsOnConnectionOptions() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+
+        connection.setProperties(null);
+        connection.setOfferedCapabilities((Symbol[]) null);
+        connection.setDesiredCapabilities((Symbol[]) null);
+        connection.setCondition(null);
+        connection.open();
+
+        assertNotNull(connection.getAttachments());
+        assertNull(connection.getProperties());
+        assertNull(connection.getOfferedCapabilities());
+        assertNull(connection.getDesiredCapabilities());
+        assertNull(connection.getCondition());
+
+        assertNull(connection.getRemoteProperties());
+        assertNull(connection.getRemoteOfferedCapabilities());
+        assertNull(connection.getRemoteDesiredCapabilities());
+        assertNull(connection.getRemoteCondition());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEndpointEmitsEngineShutdownEvent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean engineShutdown = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.engineShutdownHandler(result -> engineShutdown.set(true));
+
+        connection.open();
+        connection.close();
+
+        engine.shutdown();
+
+        assertTrue(engineShutdown.get(), "Connection should have reported engine shutdown");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionOpenAndCloseAreIdempotent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+
+        // Should not emit another open frame
+        connection.open();
+
+        connection.close();
+
+        // Should not emit another close frame
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionRemoteOpenTriggeredWhenRemoteOpenArrives() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+
+        final AtomicBoolean remoteOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler((result) -> {
+            remoteOpened.set(true);
+        });
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(remoteOpened.get());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionRemoteOpenTriggeredWhenRemoteOpenArrivesBeforeLocalOpen() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean remoteOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler((result) -> {
+            remoteOpened.set(true);
+        });
+
+        peer.expectAMQPHeader();
+
+        // Remote Header will prompt local response and then remote open should trigger
+        // the connection handler to fire so that user knows remote opened.
+        peer.remoteHeader(AMQPHeader.getAMQPHeader().toArray()).now();
+        peer.remoteOpen().now();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(remoteOpened.get());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionRemoteCloseTriggeredWhenRemoteCloseArrives() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectClose().respond();
+
+        final AtomicBoolean connectionOpenedSignaled = new AtomicBoolean();
+        final AtomicBoolean connectionClosedSignaled = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler(result -> {
+            connectionOpenedSignaled.set(true);
+        });
+        connection.closeHandler(result -> {
+            connectionClosedSignaled.set(true);
+        });
+
+        connection.open();
+
+        assertEquals(ConnectionState.ACTIVE, connection.getState());
+        assertEquals(ConnectionState.ACTIVE, connection.getRemoteState());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(ConnectionState.CLOSED, connection.getState());
+        assertEquals(ConnectionState.CLOSED, connection.getRemoteState());
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesAllSetValues() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(true, true, true, true, true);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesDefaultMaxFrameSize() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesSetMaxFrameSize() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(true, false, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesDefaultContainerId() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesSetContainerId() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, true, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesDefaultChannelMax() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesSetChannelMax() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, true, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesNoHostname() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesSetHostname() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, true, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesNoidleTimeout() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, false, false);
+    }
+
+    @Test
+    public void testConnectionOpenCarriesSetIdleTimeout() throws IOException {
+        doTestConnectionOpenPopulatesOpenCorrectly(false, false, false, false, true);
+    }
+
+    private void doTestConnectionOpenPopulatesOpenCorrectly(boolean setMaxFrameSize, boolean setContainerId, boolean setChannelMax,
+                                                            boolean setHostname, boolean setIdleTimeout) {
+        final int expectedMaxFrameSize = 32767;
+        final Matcher<?> expectedMaxFrameSizeMatcher;
+        if (setMaxFrameSize) {
+            expectedMaxFrameSizeMatcher = new UnsignedIntegerMatcher(expectedMaxFrameSize);
+        } else {
+            expectedMaxFrameSizeMatcher = new UnsignedIntegerMatcher(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE);
+        }
+
+        String expectedContainerId = "";
+        if (setContainerId) {
+            expectedContainerId = "test";
+        }
+
+        final short expectedChannelMax = 512;
+        final Matcher<?> expectedChannelMaxMatcher;
+        if (setChannelMax) {
+            expectedChannelMaxMatcher = new UnsignedShortMatcher(expectedChannelMax);
+        } else {
+            expectedChannelMaxMatcher = nullValue();
+        }
+
+        String expectedHostname = null;
+        if (setHostname) {
+            expectedHostname = "localhost";
+        }
+        final int expectedIdleTimeout = 60000;
+        final Matcher<?> expectedIdleTimeoutMatcher;
+        if (setIdleTimeout) {
+            expectedIdleTimeoutMatcher = new UnsignedIntegerMatcher(expectedIdleTimeout);
+        } else {
+            expectedIdleTimeoutMatcher = nullValue();
+        }
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(expectedMaxFrameSizeMatcher)
+                         .withChannelMax(expectedChannelMaxMatcher)
+                         .withContainerId(expectedContainerId)
+                         .withHostname(expectedHostname)
+                         .withIdleTimeOut(expectedIdleTimeoutMatcher)
+                         .withIncomingLocales(nullValue())
+                         .withOutgoingLocales(nullValue())
+                         .withDesiredCapabilities(nullValue())
+                         .withOfferedCapabilities(nullValue())
+                         .withProperties(nullValue())
+                         .respond().withContainerId("driver");
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        if (setMaxFrameSize) {
+            connection.setMaxFrameSize(expectedMaxFrameSize);
+        }
+        if (setContainerId) {
+            connection.setContainerId(expectedContainerId);
+        }
+        if (setChannelMax) {
+            connection.setChannelMax(expectedChannelMax);
+        }
+        if (setHostname) {
+            connection.setHostname(expectedHostname);
+        }
+        if (setIdleTimeout) {
+            connection.setIdleTimeout(expectedIdleTimeout);
+        }
+
+        assertEquals(expectedContainerId, connection.getContainerId());
+        assertEquals(expectedHostname, connection.getHostname());
+
+        connection.open();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCapabilitiesArePopulatedAndAccessible() throws Exception {
+        final Symbol clientOfferedSymbol = Symbol.valueOf("clientOfferedCapability");
+        final Symbol clientDesiredSymbol = Symbol.valueOf("clientDesiredCapability");
+        final Symbol serverOfferedSymbol = Symbol.valueOf("serverOfferedCapability");
+        final Symbol serverDesiredSymbol = Symbol.valueOf("serverDesiredCapability");
+
+        Symbol[] clientOfferedCapabilities = new Symbol[] { clientOfferedSymbol };
+        Symbol[] clientDesiredCapabilities = new Symbol[] { clientDesiredSymbol };
+        Symbol[] clientExpectedOfferedCapabilities = new Symbol[] { serverOfferedSymbol };
+        Symbol[] clientExpectedDesiredCapabilities = new Symbol[] { serverDesiredSymbol };
+
+        final AtomicBoolean remotelyOpened = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withOfferedCapabilities(new String[] { clientOfferedSymbol.toString() })
+                         .withDesiredCapabilities(new String[] { clientDesiredSymbol.toString() })
+                         .respond()
+                         .withDesiredCapabilities(new String[] { serverDesiredSymbol.toString() })
+                         .withOfferedCapabilities(new String[] { serverOfferedSymbol.toString() });
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.setDesiredCapabilities(clientDesiredCapabilities);
+        connection.setOfferedCapabilities(clientOfferedCapabilities);
+        connection.openHandler(result -> {
+            remotelyOpened.set(true);
+        });
+        connection.open();
+
+        assertTrue(remotelyOpened.get(), "Connection remote opened event did not fire");
+
+        assertArrayEquals(clientOfferedCapabilities, connection.getOfferedCapabilities());
+        assertArrayEquals(clientDesiredCapabilities, connection.getDesiredCapabilities());
+        assertArrayEquals(clientExpectedOfferedCapabilities, connection.getRemoteOfferedCapabilities());
+        assertArrayEquals(clientExpectedDesiredCapabilities, connection.getRemoteDesiredCapabilities());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testPropertiesArePopulatedAndAccessible() throws Exception {
+        final Symbol clientPropertyName = Symbol.valueOf("ClientPropertyName");
+        final Integer clientPropertyValue = 1234;
+        final Symbol serverPropertyName = Symbol.valueOf("ServerPropertyName");
+        final Integer serverPropertyValue = 5678;
+
+        Map<String, Object> clientPropertiesMap = new HashMap<>();
+        clientPropertiesMap.put(clientPropertyName.toString(), clientPropertyValue);
+
+        Map<String, Object> serverPropertiesMap = new HashMap<>();
+        serverPropertiesMap.put(serverPropertyName.toString(), serverPropertyValue);
+
+        final AtomicBoolean remotelyOpened = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withProperties(clientPropertiesMap)
+                         .respond()
+                         .withProperties(serverPropertiesMap);
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        Map<Symbol, Object> clientProperties = new HashMap<>();
+        clientProperties.put(clientPropertyName, clientPropertyValue);
+
+        connection.setProperties(clientProperties);
+        connection.openHandler(result -> {
+            remotelyOpened.set(true);
+        });
+        connection.open();
+
+        assertTrue(remotelyOpened.get(), "Connection remote opened event did not fire");
+
+        assertNotNull(connection.getProperties());
+        assertNotNull(connection.getRemoteProperties());
+
+        assertEquals(clientPropertyValue, connection.getProperties().get(clientPropertyName));
+        assertEquals(serverPropertyValue, connection.getRemoteProperties().get(serverPropertyName));
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenedCarriesRemoteErrorCondition() throws Exception {
+        Map<String, Object> errorInfoExpectation = new HashMap<>();
+        errorInfoExpectation.put("error", "value");
+        errorInfoExpectation.put("error-list", Arrays.asList("entry-1", "entry-2", "entry-3"));
+
+        Map<Symbol, Object> errorInfo = new HashMap<>();
+        errorInfo.put(Symbol.getSymbol("error"), "value");
+        errorInfo.put(Symbol.getSymbol("error-list"), Arrays.asList("entry-1", "entry-2", "entry-3"));
+        ErrorCondition remoteCondition = new ErrorCondition(Symbol.getSymbol("myerror"), "mydescription", errorInfo);
+
+        final AtomicBoolean remotelyOpened = new AtomicBoolean();
+        final AtomicBoolean remotelyClosed = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.remoteClose().withErrorCondition("myerror", "mydescription", errorInfoExpectation).queue();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+
+        connection.openHandler(result -> {
+            remotelyOpened.set(true);
+        });
+        connection.closeHandler(result -> {
+            remotelyClosed.set(true);
+        });
+        connection.open();
+
+        assertTrue(connection.isLocallyOpen());
+        assertFalse(connection.isLocallyClosed());
+        assertFalse(connection.isRemotelyOpen());
+        assertTrue(connection.isRemotelyClosed());
+
+        assertTrue(remotelyOpened.get(), "Connection remote opened event did not fire");
+        assertTrue(remotelyClosed.get(), "Connection remote closed event did not fire");
+
+        assertNull(connection.getCondition());
+        assertNotNull(connection.getRemoteCondition());
+
+        assertEquals(remoteCondition, connection.getRemoteCondition());
+
+        connection.close();
+
+        assertFalse(connection.isLocallyOpen());
+        assertTrue(connection.isLocallyClosed());
+        assertFalse(connection.isRemotelyOpen());
+        assertTrue(connection.isRemotelyClosed());
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEmptyFrameBeforeOpenDoesNotCauseError() throws Exception {
+        final AtomicBoolean remotelyOpened = new AtomicBoolean();
+        final AtomicBoolean remotelyClosed = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.remoteEmptyFrame().queue();
+        peer.remoteOpen().queue();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.openHandler(result -> {
+            remotelyOpened.set(true);
+        });
+        connection.closeHandler(result -> {
+            remotelyClosed.set(true);
+        });
+        connection.open();
+
+        assertTrue(connection.isLocallyOpen());
+        assertFalse(connection.isLocallyClosed());
+        assertTrue(connection.isRemotelyOpen());
+        assertFalse(connection.isRemotelyClosed());
+
+        assertTrue(remotelyOpened.get(), "Connection remote opened event did not fire");
+
+        connection.close();
+
+        assertTrue(remotelyClosed.get(), "Connection remote closed event did not fire");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testChannelMaxDefaultsToMax() throws Exception {
+        final AtomicBoolean remotelyOpened = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withChannelMax(nullValue()).respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.openHandler(result -> {
+            remotelyOpened.set(true);
+        });
+        connection.open();
+
+        assertTrue(remotelyOpened.get(), "Connection remote opened event did not fire");
+
+        assertEquals(65535, connection.getChannelMax());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testChannelMaxRangeEnforced() throws Exception {
+        final AtomicBoolean remotelyOpened = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final short eventualChannelMax = 255;
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withChannelMax(eventualChannelMax).respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.openHandler(result -> {
+            remotelyOpened.set(true);
+        });
+
+        try {
+            connection.setChannelMax(-1);
+            fail("Should not be able to set an invalid negative channel max");
+        } catch (IllegalArgumentException iae) {}
+        try {
+            connection.setChannelMax(65536);
+            fail("Should not be able to set an invalid to large channel max");
+        } catch (IllegalArgumentException iae) {}
+
+        connection.setChannelMax(eventualChannelMax);
+        connection.open();
+
+        assertTrue(remotelyOpened.get(), "Connection remote opened event did not fire");
+        assertEquals(eventualChannelMax, connection.getChannelMax());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseConnectionAfterShutdownDoesNotThrowExceptionOpenWrittenAndResponse() throws Exception {
+        testCloseConnectionAfterShutdownNoOutputAndNoException(true, true);
+    }
+
+    @Test
+    public void testCloseConnectionAfterShutdownDoesNotThrowExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseConnectionAfterShutdownNoOutputAndNoException(true, false);
+    }
+
+    @Test
+    public void testCloseConnectionAfterShutdownDoesNotThrowExceptionOpenNotWritten() throws Exception {
+        testCloseConnectionAfterShutdownNoOutputAndNoException(false, false);
+    }
+
+    private void testCloseConnectionAfterShutdownNoOutputAndNoException(boolean respondToHeader, boolean respondToOpen) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+            } else {
+                peer.expectOpen();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        engine.shutdown();
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseConnectionAfterFailureThrowsEngineStateExceptionOpenWrittenAndResponse() throws Exception {
+        testCloseConnectionAfterEngineFailedThrowsAndNoOutputWritten(true, true);
+    }
+
+    @Test
+    public void testCloseConnectionAfterFailureThrowsEngineStateExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseConnectionAfterEngineFailedThrowsAndNoOutputWritten(true, false);
+    }
+
+    @Test
+    public void testCloseConnectionAfterFailureThrowsEngineStateExceptionOpenNotWritten() throws Exception {
+        testCloseConnectionAfterEngineFailedThrowsAndNoOutputWritten(false, false);
+    }
+
+    private void testCloseConnectionAfterEngineFailedThrowsAndNoOutputWritten(boolean respondToHeader, boolean respondToOpen) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                peer.expectClose();
+            } else {
+                peer.expectOpen();
+                peer.expectClose();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        engine.engineFailed(new IOException());
+
+        try {
+            connection.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        engine.shutdown();  // Explicit shutdown now allows local close to complete
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testOpenAndCloseWhileWaitingForHeaderResponseDoesNotWriteUntilHeaderArrives() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader();
+
+        Connection connection = engine.start();
+        connection.open();  // Trigger write of AMQP Header, we don't respond here.
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        // Now respond and Connection should open and close
+        peer.expectOpen();
+        peer.expectClose();
+        peer.remoteHeader(AMQPHeader.getAMQPHeader().toArray()).now();
+
+        peer.waitForScriptToComplete();
+
+        engine.shutdown();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenWhileWaitingForHeaderResponseDoesNotWriteThenWritesFlowAsExpected() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader();
+
+        Connection connection = engine.start();
+        connection.open();  // Trigger write of AMQP Header, we don't respond here.
+
+        peer.waitForScriptToComplete();
+
+        // Now respond and Connection should open and close
+        peer.expectOpen();
+        peer.expectClose().withError(notNullValue());
+        peer.remoteHeader(AMQPHeader.getAMQPHeader().toArray()).now();
+
+        connection.setCondition(new ErrorCondition(ConnectionError.CONNECTION_FORCED, "something about errors")).close();
+
+        peer.waitForScriptToComplete();
+
+        engine.shutdown();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseOrDetachWithErrorCondition() throws Exception {
+        final String condition = "amqp:connection:forced";
+        final String description = "something bad happened.";
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().withError(condition, description).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        connection.setCondition(new ErrorCondition(Symbol.valueOf(condition), description));
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotCreateSessionFromLocallyClosedConnection() throws Exception {
+        testCannotCreateSessionFromClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotCreateSessionFromRemotelyClosedConnection() throws Exception {
+        testCannotCreateSessionFromClosedConnection(false);
+    }
+
+    private void testCannotCreateSessionFromClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.session();
+            fail("Should not create new Session from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetContainerIdOnOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setContainerId("test");
+            fail("Should not be able to set container ID from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetContainerIdOnLocallyClosedConnection() throws Exception {
+        testCannotSetContainerIdOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetContainerIdOnRemotelyClosedConnection() throws Exception {
+        testCannotSetContainerIdOnClosedConnection(false);
+    }
+
+    private void testCannotSetContainerIdOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setContainerId("test");
+            fail("Should not be able to set container ID from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetHostnameOnOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setHostname("test");
+            fail("Should not be able to set host name from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetHostnameOnLocallyClosedConnection() throws Exception {
+        testCannotSetHostnameOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetHostnameOnRemotelyClosedConnection() throws Exception {
+        testCannotSetHostnameOnClosedConnection(false);
+    }
+
+    private void testCannotSetHostnameOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setHostname("test");
+            fail("Should not be able to set host name from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetChannelMaxOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setChannelMax(0);
+            fail("Should not be able to set channel max from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetChannelMaxOnLocallyClosedConnection() throws Exception {
+        testCannotSetChannelMaxOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetChannelMaxOnRemotelyClosedConnection() throws Exception {
+        testCannotSetChannelMaxOnClosedConnection(false);
+    }
+
+    private void testCannotSetChannelMaxOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setChannelMax(0);
+            fail("Should not be able to set channel max from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetMaxFrameSizeOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setMaxFrameSize(65535);
+            fail("Should not be able to set max frame size from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetMaxFrameSizeOnLocallyClosedConnection() throws Exception {
+        testCannotSetMaxFrameSizeOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetMaxFrameSizeOnRemotelyClosedConnection() throws Exception {
+        testCannotSetMaxFrameSizeOnClosedConnection(false);
+    }
+
+    private void testCannotSetMaxFrameSizeOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setMaxFrameSize(65535);
+            fail("Should not be able to set max frame size from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetIdleTimeoutOnOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+
+        try {
+            connection.setIdleTimeout(-1);
+            fail("Should not be able to set idle timeout when negative value given");
+        } catch (IllegalArgumentException error) {
+            // Expected
+        }
+        try {
+            connection.setIdleTimeout(Long.MAX_VALUE);
+            fail("Should not be able to set idle timeout greater than unsigned integer value");
+        } catch (IllegalArgumentException error) {
+            // Expected
+        }
+
+        connection.open();
+
+        assertEquals(0, connection.getIdleTimeout());
+
+        try {
+            connection.setIdleTimeout(65535);
+            fail("Should not be able to set idle timeout from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        assertEquals(0, connection.getIdleTimeout());
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetIdleTimeoutOnLocallyClosedConnection() throws Exception {
+        testCannotSetIdleTimeoutOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetIdleTimeoutOnRemotelyClosedConnection() throws Exception {
+        testCannotSetIdleTimeoutOnClosedConnection(false);
+    }
+
+    private void testCannotSetIdleTimeoutOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setIdleTimeout(65535);
+            fail("Should not be able to set idle timeout from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetOfferedCapabilitiesOnOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setOfferedCapabilities(Symbol.valueOf("ANONYMOUS_RELAY"));
+            fail("Should not be able to set offered capabilities from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetOfferedCapabilitiesOnLocallyClosedConnection() throws Exception {
+        testCannotSetOfferedCapabilitiesOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetOfferedCapabilitiesOnRemotelyClosedConnection() throws Exception {
+        testCannotSetOfferedCapabilitiesOnClosedConnection(false);
+    }
+
+    private void testCannotSetOfferedCapabilitiesOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setOfferedCapabilities(Symbol.valueOf("ANONYMOUS_RELAY"));
+            fail("Should not be able to set offered capabilities from closed Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetDesiredCapabilitiesOnOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setDesiredCapabilities(Symbol.valueOf("ANONYMOUS_RELAY"));
+            fail("Should not be able to set desired capabilities from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetDesiredCapabilitiesOnLocallyClosedConnection() throws Exception {
+        testCannotSetDesiredCapabilitiesOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetDesiredCapabilitiesOnRemotelyClosedConnection() throws Exception {
+        testCannotSetDesiredCapabilitiesOnClosedConnection(false);
+    }
+
+    private void testCannotSetDesiredCapabilitiesOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setDesiredCapabilities(Symbol.valueOf("ANONYMOUS_RELAY"));
+            fail("Should not be able to set desired capabilities from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetPropertiesOnOpenConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        try {
+            connection.setProperties(Collections.emptyMap());
+            fail("Should not be able to set properties from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSetPropertiesOnLocallyClosedConnection() throws Exception {
+        testCannotSetPropertiesOnClosedConnection(true);
+    }
+
+    @Test
+    public void testCannotSetPropertiesOnRemotelyClosedConnection() throws Exception {
+        testCannotSetPropertiesOnClosedConnection(false);
+    }
+
+    private void testCannotSetPropertiesOnClosedConnection(boolean localClose) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        if (localClose) {
+            peer.expectClose().respond();
+        } else {
+            peer.remoteClose().queue();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+        if (localClose) {
+            connection.close();
+        }
+
+        try {
+            connection.setProperties(Collections.emptyMap());
+            fail("Should not be able to set properties from open Connection");
+        } catch (IllegalStateException error) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testIterateAndCloseSessionsFromSessionsAPI() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectBegin().respond();
+        peer.expectBegin().respond();
+
+        Connection connection = engine.start().open();
+
+        connection.session().open();
+        connection.session().open();
+        connection.session().open();
+
+        peer.waitForScriptToComplete();
+
+        peer.expectEnd().respond();
+        peer.expectEnd().respond();
+        peer.expectEnd().respond();
+        peer.expectClose();
+
+        connection.sessions().forEach(session -> session.close());
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(connection.sessions().isEmpty());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionClosedWhenChannelMaxExceeded() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean closed = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withChannelMax(16).respond();
+        peer.expectClose().withError(ConnectionError.FRAMING_ERROR.toString(), "Channel Max Exceeded for session Begin").respond();
+
+        Connection connection = engine.start();
+
+        connection.setChannelMax(16);
+        connection.localCloseHandler(conn -> {
+            closed.set(true);
+        });
+        connection.open();
+
+        peer.remoteBegin().onChannel(32).now();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(connection.sessions().isEmpty());
+        assertTrue(closed.get());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionThrowsWhenLocalChannelMaxExceeded() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withChannelMax(1).respond();
+        peer.expectBegin().onChannel(0).respond().onChannel(1);
+        peer.expectBegin().onChannel(1).respond().onChannel(0);
+        peer.expectEnd().onChannel(1).respond().onChannel(0);
+
+        Connection connection = engine.start();
+
+        connection.setChannelMax(1);
+        connection.open();
+
+        Session session1 = connection.session().open();
+        Session session2 = connection.session().open();
+
+        try {
+            connection.session().open();
+            fail("Should not be able to exceed local channel max");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        session2.close();
+
+        peer.waitForScriptToComplete();
+        peer.expectBegin().onChannel(1).respond().onChannel(0);
+        peer.expectEnd().onChannel(0).respond().onChannel(1);
+        peer.expectEnd().onChannel(1).respond().onChannel(0);
+        peer.expectClose().respond();
+
+        session2 = connection.session().open();
+        session1.close();
+        session2.close();
+
+        connection.close();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNoOpenWrittenAfterEncodeErrorFromConnectionProperties() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), engine);
+
+        Connection connection = engine.start().setProperties(properties);
+
+        // Ensures that open is synchronous as header exchange will be complete.
+        connection.negotiate();
+
+        try {
+            connection.open();
+            fail("Should not have been able to open with invalid type in properties");
+        } catch (FrameEncodingException fee) {
+            assertTrue(fee.getCause() instanceof EncodeException);
+            engine.shutdown();
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testPipelinedResourceOpenAllowsForReturningResponsesAfterCloseOfConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin();
+        peer.expectEnd();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+        peer.expectClose();
+        peer.remoteBegin().withRemoteChannel(0)
+                          .withNextOutgoingId(0).queue();
+        peer.remoteClose().queue();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSecondOpenAfterReceiptOfFirstFailsEngine() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().withError(notNullValue());
+
+        engine.start();
+
+        peer.remoteOpen().onChannel(0).now();
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testUnexpectedEndFrameFailsEngine() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().withError(notNullValue());
+
+        engine.start();
+
+        peer.remoteEnd().onChannel(10).now();
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testUnexpectedAttachForUnknownChannelFrameFailsEngine() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().withError(notNullValue());
+
+        engine.start();
+
+        peer.remoteAttach().ofSender().withName("test").withHandle(0).onChannel(10).now();
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testUnexpectedDetachForUnknownChannelFrameFailsEngine() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().withError(notNullValue());
+
+        engine.start();
+
+        peer.remoteDetach().withHandle(0).onChannel(10).now();
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testSecondBeginForAlreadyBegunSessionTriggerEngineFailure() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection();
+        connection.session().open();
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().onChannel(0).respond().onChannel(0);
+        peer.expectClose().withError(notNullValue());
+
+        engine.start();
+
+        peer.remoteBegin().onChannel(0).now();
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonDecodeErrorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonDecodeErrorTest.java
new file mode 100644
index 0000000..924d36b
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonDecodeErrorTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.FrameDecodingException;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Timeout(20)
+public class ProtonDecodeErrorTest extends ProtonEngineTestSupport {
+
+    @Test
+    public void testEmptyContainerIdInOpenProvokesDecodeError() throws Exception {
+        // Provide the bytes for Open, but omit the mandatory container-id to provoke a decode error.
+        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
+                                     0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+                                     0x00, 0x53, 0x10, (byte) 0xC0, // Described-type, ulong type, open descriptor, list0.
+                                     0x03, 0x01, 0x40 }; // size (3), count (1), container-id (null).
+
+        doInvalidOpenProvokesDecodeErrorTestImpl(bytes, "The container-id field cannot be omitted");
+    }
+
+    @Test
+    public void testEmptyOpenProvokesDecodeError() throws Exception {
+        // Provide the bytes for Open, but omit the mandatory container-id to provoke a decode error.
+        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
+                                     0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+                                     0x00, 0x53, 0x10, 0x45};// Described-type, ulong type, open descriptor, list0.
+
+        doInvalidOpenProvokesDecodeErrorTestImpl(bytes, "The container-id field cannot be omitted");
+    }
+
+    private void doInvalidOpenProvokesDecodeErrorTestImpl(byte[] bytes, String errorDescription) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+
+        engine.start().open();
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        peer.expectClose().withError(AmqpError.DECODE_ERROR.toString(), errorDescription);
+        peer.remoteBytes().withBytes(bytes).now();
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof FrameDecodingException);
+        assertEquals(errorDescription, failure.getMessage());
+    }
+
+    @Test
+    public void testEmptyBeginProvokesDecodeError() throws Exception {
+        // Provide the bytes for Begin, but omit any fields to provoke a decode error.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x11, 0x45};// Described-type, ulong type, Begin descriptor, list0.
+
+        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The next-outgoing-id field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedBeginProvokesDecodeError1() throws Exception {
+        // Provide the bytes for Begin, but only give a null (i-e not-present) for the remote-channel.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x11, (byte) 0xC0, // Described-type, ulong type, Begin descriptor, list8.
+            0x03, 0x01, 0x40 }; // size (3), count (1), remote-channel (null).
+
+        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The next-outgoing-id field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedBeginProvokesDecodeError2() throws Exception {
+        // Provide the bytes for Begin, but only give a [not-present remote-channel +] next-outgoing-id and incoming-window. Provokes a decode error as there must be 4 fields.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x11, // Frame size = 17 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x11, (byte) 0xC0, // Described-type, ulong type, Begin descriptor, list8.
+            0x05, 0x03, 0x40, 0x43, 0x43 }; // size (5), count (3), remote-channel (null), next-outgoing-id (uint0), incoming-window (uint0).
+
+        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedBeginProvokesDecodeError3() throws Exception {
+        // Provide the bytes for Begin, but only give a [not-present remote-channel +] next-outgoing-id and incoming-window, and send not-present/null for outgoing-window. Provokes a decode error as must be present.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x12, // Frame size = 18 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x11, (byte) 0xC0, // Described-type, ulong type, Begin descriptor, list8.
+            0x06, 0x04, 0x40, 0x43, 0x43, 0x40 }; // size (5), count (4), remote-channel (null), next-outgoing-id (uint0), incoming-window (uint0), outgoing-window (null).
+
+        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
+    }
+
+    private void doInvalidBeginProvokesDecodeErrorTestImpl(byte[] bytes, String errorDescription) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        engine.start().open();
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        peer.expectClose().withError(AmqpError.DECODE_ERROR.toString(), errorDescription);
+        peer.remoteBytes().withBytes(bytes).now();
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof FrameDecodingException);
+        assertEquals(errorDescription, failure.getMessage());
+    }
+
+    @Test
+    public void testEmptyFlowProvokesDecodeError() throws Exception {
+        // Provide the bytes for Flow, but omit any fields to provoke a decode error.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x13, 0x45};// Described-type, ulong type, Flow descriptor, list0.
+
+        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The incoming-window field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedFlowProvokesDecodeError1() throws Exception {
+        // Provide the bytes for Flow, but only give a 0 for the next-incoming-id. Provokes a decode error as there must be 4 fields.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x13, (byte) 0xC0, // Described-type, ulong type, Flow descriptor, list8.
+            0x03, 0x01, 0x43 }; // size (3), count (1), next-incoming-id (uint0).
+
+        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The incoming-window field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedFlowProvokesDecodeError2() throws Exception {
+        // Provide the bytes for Flow, but only give a next-incoming-id and incoming-window and next-outgoing-id. Provokes a decode error as there must be 4 fields.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x11, // Frame size = 17 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x13, (byte) 0xC0, // Described-type, ulong type, Flow descriptor, list8.
+            0x05, 0x03, 0x43, 0x43, 0x43 }; // size (5), count (3), next-incoming-id (0), incoming-window (uint0), next-outgoing-id (uint0).
+
+        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedFlowProvokesDecodeError3() throws Exception {
+        // Provide the bytes for Flow, but only give a next-incoming-id and incoming-window and next-outgoing-id, and send not-present/null for outgoing-window. Provokes a decode error as must be present.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x12, // Frame size = 18 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x13, (byte) 0xC0, // Described-type, ulong type, Flow descriptor, list8.
+            0x06, 0x04, 0x43, 0x43, 0x43, 0x40 }; // size (5), count (4), next-incoming-id (0), incoming-window (uint0), next-outgoing-id (uint0), outgoing-window (null).
+
+        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
+    }
+
+    private void doInvalidFlowProvokesDecodeErrorTestImpl(byte[] bytes, String errorDescription) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteBytes().withBytes(bytes).queue();  // Queue the frame for write after expected setup
+        peer.expectClose().withError(AmqpError.DECODE_ERROR.toString(), errorDescription);
+
+        Connection connection = engine.start().open();
+        connection.session().open();
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof FrameDecodingException);
+        assertEquals(errorDescription, failure.getMessage());
+    }
+
+    @Test
+    public void testEmptyTransferProvokesDecodeError() throws Exception {
+        // Provide the bytes for Transfer, but omit any fields to provoke a decode error.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x14, 0x45};// Described-type, ulong type, Transfer descriptor, list0.
+
+        doInvalidTransferProvokesDecodeErrorTestImpl(bytes, "The handle field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedTransferProvokesDecodeError() throws Exception {
+        // Provide the bytes for Transfer, but only give a null for the not-present handle. Provokes a decode error as there must be a handle.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x14, (byte) 0xC0, // Described-type, ulong type, Transfer descriptor, list8.
+            0x03, 0x01, 0x40 }; // size (3), count (1), handle (null / not-present).
+
+        doInvalidTransferProvokesDecodeErrorTestImpl(bytes, "The handle field cannot be omitted");
+    }
+
+
+    @Test
+    public void testTransferWithWrongHandleTypeCodeProvokesDecodeError() throws Exception {
+        // Provide the bytes for Transfer, but give the wrong type code for a not-really-present handle. Provokes a decode error.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x14, (byte) 0xC0, // Described-type, ulong type, Transfer descriptor, list8.
+            0x03, 0x01, (byte) 0xA3 }; // size (3), count (1), handle (invalid sym8 type constructor given, not really present).
+
+        doInvalidTransferProvokesDecodeErrorTestImpl(bytes, "Expected Unsigned Integer type but found encoding: SYM8:0xa3");
+    }
+
+    private void doInvalidTransferProvokesDecodeErrorTestImpl(byte[] bytes, String errorDescription) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(1);
+        peer.remoteBytes().withBytes(bytes).queue();  // Queue the frame for write after expected setup
+        peer.expectClose().withError(AmqpError.DECODE_ERROR.toString(), errorDescription);
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open();
+
+        receiver.addCredit(1);
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof FrameDecodingException);
+        assertEquals(errorDescription, failure.getMessage());
+    }
+
+    @Test
+    public void testEmptyDispositionProvokesDecodeError() throws Exception {
+        // Provide the bytes for Disposition, but omit any fields to provoke a decode error.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x15, 0x45};// Described-type, ulong type, Disposition descriptor, list0.
+
+        doInvalidDispositionProvokesDecodeErrorTestImpl(bytes, "The role field cannot be omitted");
+    }
+
+    @Test
+    public void testTruncatedDispositionProvokesDecodeError() throws Exception {
+        // Provide the bytes for Disposition, but only give a null/not-present for the 'first' field. Provokes a decode error as there must be a role and 'first'.
+        byte[] bytes = new byte[] {
+            0x00, 0x00, 0x00, 0x10, // Frame size = 16 bytes.
+            0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
+            0x00, 0x53, 0x15, (byte) 0xC0, // Described-type, ulong type, Disposition descriptor, list8.
+            0x04, 0x02, 0x41, 0x40 }; // size (4), count (2), role (receiver - the peers perspective), first ( null / not-present)
+
+        doInvalidDispositionProvokesDecodeErrorTestImpl(bytes, "The first field cannot be omitted");
+    }
+
+    private void doInvalidDispositionProvokesDecodeErrorTestImpl(byte[] bytes, String errorDescription) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(10).queue();
+        peer.expectTransfer();
+        peer.remoteBytes().withBytes(bytes).queue();  // Queue the frame for write after expected setup
+        peer.expectClose().withError(AmqpError.DECODE_ERROR.toString(), errorDescription);
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        OutgoingDelivery delivery = sender.next();
+        delivery.writeBytes(payload.duplicate());
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof FrameDecodingException);
+        assertEquals(errorDescription, failure.getMessage());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonDeliveryTagGeneratorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonDeliveryTagGeneratorTest.java
new file mode 100644
index 0000000..fad0c4b
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonDeliveryTagGeneratorTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.junit.jupiter.api.Test;
+
+public class ProtonDeliveryTagGeneratorTest {
+
+    @Test
+    public void testEmptyTagGenerator() {
+        DeliveryTagGenerator tagGen1 = ProtonDeliveryTagGenerator.BUILTIN.EMPTY.createGenerator();
+        DeliveryTagGenerator tagGen2 = ProtonDeliveryTagGenerator.BUILTIN.EMPTY.createGenerator();
+
+        assertSame(tagGen1, tagGen2);
+
+        DeliveryTag tag1 = tagGen1.nextTag();
+        DeliveryTag tag2 = tagGen2.nextTag();
+
+        assertSame(tag1, tag2);
+
+        assertEquals(0, tag1.tagLength());
+        assertNotNull(tag1.tagBytes());
+
+        assertNotNull(tagGen1.toString());
+        assertNotNull(tagGen2.toString());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineFactoryTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineFactoryTest.java
new file mode 100644
index 0000000..dbd73d1
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineFactoryTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the ProtonEngineFactory implementation.
+ */
+public class ProtonEngineFactoryTest {
+
+    @Test
+    public void testCreateEngine() {
+        Engine engine = EngineFactory.PROTON.createEngine();
+
+        assertEquals(EngineState.IDLE, engine.state());
+        assertNotNull(engine.saslDriver());
+        assertEquals(engine.saslDriver().getSaslState(), SaslState.IDLE);
+    }
+
+    @Test
+    public void testCreateNonSaslEngine() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+
+        assertEquals(EngineState.IDLE, engine.state());
+        assertNotNull(engine.saslDriver());
+        assertEquals(engine.saslDriver().getSaslState(), SaslState.NONE);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipelineTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipelineTest.java
new file mode 100644
index 0000000..5d3b191
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEnginePipelineTest.java
@@ -0,0 +1,422 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.util.FrameReadSinkTransportHandler;
+import org.apache.qpid.protonj2.engine.util.FrameWriteSinkTransportHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class ProtonEnginePipelineTest {
+
+    private ProtonEngine engine;
+
+    @BeforeEach
+    public void initMocks() {
+        engine = Mockito.mock(ProtonEngine.class);
+    }
+
+    @Test
+    public void testCreatePipeline() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        assertSame(pipeline.engine(), engine);
+        assertNull(pipeline.first());
+        assertNull(pipeline.last());
+        assertNull(pipeline.firstContext());
+        assertNull(pipeline.lastContext());
+    }
+
+    @Test
+    public void testCreatePipelineRejectsNullParent() {
+        try {
+            new ProtonEnginePipeline(null);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    //----- Tests for addFirst -----------------------------------------------//
+
+    @Test
+    public void testAddFirstRejectsNullHandler() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        try {
+            pipeline.addFirst("one", null);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAddFirstRejectsNullHandlerName() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        try {
+            pipeline.addFirst(null, handler);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAddFirstRejectsEmptyHandlerName() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        try {
+            pipeline.addFirst("", handler);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAddFirstWithOneHandler() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("one", handler);
+
+        assertSame(handler, pipeline.first());
+        assertSame(handler, pipeline.last());
+        assertNotNull(pipeline.firstContext());
+        assertSame(handler, pipeline.firstContext().handler());
+    }
+
+    @Test
+    public void testAddFirstWithMoreThanOneHandler() {
+        EngineHandler handler1 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler2 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler3 = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("three", handler3);
+        pipeline.addFirst("two", handler2);
+        pipeline.addFirst("one", handler1);
+
+        assertSame(handler1, pipeline.first());
+        assertSame(handler3, pipeline.last());
+
+        assertNotNull(pipeline.firstContext());
+        assertSame(handler1, pipeline.firstContext().handler());
+        assertNotNull(pipeline.lastContext());
+        assertSame(handler3, pipeline.lastContext().handler());
+    }
+
+    //----- Tests for addLast ------------------------------------------------//
+
+    @Test
+    public void testAddLastRejectsNullHandler() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        try {
+            pipeline.addLast("one", null);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAddLastRejectsNullHandlerName() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        try {
+            pipeline.addLast(null, handler);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAddLastRejectsEmptyHandlerName() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        try {
+            pipeline.addLast("", handler);
+            fail("Should throw an IllegalArgumentException");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testAddLastWithOneHandler() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addLast("one", handler);
+
+        assertSame(handler, pipeline.first());
+        assertSame(handler, pipeline.last());
+
+        assertNotNull(pipeline.firstContext());
+        assertSame(handler, pipeline.firstContext().handler());
+        assertNotNull(pipeline.lastContext());
+        assertSame(handler, pipeline.lastContext().handler());
+    }
+
+    @Test
+    public void testAddLastWithMoreThanOneHandler() {
+        EngineHandler handler1 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler2 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler3 = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addLast("one", handler1);
+        pipeline.addLast("two", handler2);
+        pipeline.addLast("three", handler3);
+
+        assertSame(handler1, pipeline.first());
+        assertSame(handler3, pipeline.last());
+
+        assertNotNull(pipeline.firstContext());
+        assertSame(handler1, pipeline.firstContext().handler());
+        assertNotNull(pipeline.lastContext());
+        assertSame(handler3, pipeline.lastContext().handler());
+    }
+
+    //----- Tests for removeFirst --------------------------------------------//
+
+    @Test
+    public void testRemoveFirstWithOneHandler() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("one", handler);
+
+        assertNotNull(pipeline.first());
+        assertSame(pipeline, pipeline.removeFirst());
+        assertNull(pipeline.first());
+        // calling when empty should not throw.
+        assertSame(pipeline, pipeline.removeFirst());
+    }
+
+    @Test
+    public void testRemoveFirstWithMoreThanOneHandler() {
+        EngineHandler handler1 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler2 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler3 = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("three", handler3);
+        pipeline.addFirst("two", handler2);
+        pipeline.addFirst("one", handler1);
+
+        assertSame(pipeline, pipeline.removeFirst());
+        assertSame(handler2, pipeline.first());
+        assertSame(pipeline, pipeline.removeFirst());
+        assertSame(handler3, pipeline.first());
+        assertSame(pipeline, pipeline.removeFirst());
+        // calling when empty should not throw.
+        assertSame(pipeline, pipeline.removeFirst());
+        assertNull(pipeline.first());
+    }
+
+    //----- Tests for removeLast ---------------------------------------------//
+
+    @Test
+    public void testRemoveLastWithOneHandler() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("one", handler);
+
+        assertNotNull(pipeline.first());
+        assertSame(pipeline, pipeline.removeLast());
+        assertNull(pipeline.first());
+        // calling when empty should not throw.
+        assertSame(pipeline, pipeline.removeLast());
+    }
+
+    @Test
+    public void testRemoveLastWithMoreThanOneHandler() {
+        EngineHandler handler1 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler2 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler3 = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("three", handler3);
+        pipeline.addFirst("two", handler2);
+        pipeline.addFirst("one", handler1);
+
+        assertSame(pipeline, pipeline.removeLast());
+        assertSame(handler2, pipeline.last());
+        assertSame(pipeline, pipeline.removeLast());
+        assertSame(handler1, pipeline.last());
+        assertSame(pipeline, pipeline.removeLast());
+        // calling when empty should not throw.
+        assertSame(pipeline, pipeline.removeLast());
+        assertNull(pipeline.last());
+    }
+
+    //----- Tests for removeLast ---------------------------------------------//
+
+    @Test
+    public void testRemoveWhenEmpty() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        assertSame(pipeline, pipeline.remove("unknown"));
+        assertSame(pipeline, pipeline.remove(""));
+        assertSame(pipeline, pipeline.remove((String) null));
+        assertSame(pipeline, pipeline.remove((EngineHandler) null));
+    }
+
+    @Test
+    public void testRemoveWithOneHandler() {
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("one", handler);
+
+        assertSame(handler, pipeline.first());
+
+        assertSame(pipeline, pipeline.remove("unknown"));
+        assertSame(pipeline, pipeline.remove(""));
+        assertSame(pipeline, pipeline.remove((String) null));
+        assertSame(pipeline, pipeline.remove((EngineHandler) null));
+
+        assertSame(handler, pipeline.first());
+        assertSame(pipeline, pipeline.remove("one"));
+
+        assertNull(pipeline.first());
+        assertNull(pipeline.last());
+
+        pipeline.addFirst("one", handler);
+
+        assertSame(handler, pipeline.first());
+
+        assertSame(pipeline, pipeline.remove(handler));
+
+        assertNull(pipeline.first());
+        assertNull(pipeline.last());
+    }
+
+    @Test
+    public void testRemoveWithMoreThanOneHandler() {
+        EngineHandler handler1 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler2 = Mockito.mock(EngineHandler.class);
+        EngineHandler handler3 = Mockito.mock(EngineHandler.class);
+
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        pipeline.addFirst("three", handler3);
+        pipeline.addFirst("two", handler2);
+        pipeline.addFirst("one", handler1);
+
+        assertSame(handler1, pipeline.first());
+        assertSame(pipeline, pipeline.remove("one"));
+        assertSame(handler2, pipeline.first());
+        assertSame(pipeline, pipeline.remove("two"));
+        assertSame(handler3, pipeline.first());
+        assertSame(pipeline, pipeline.remove("three"));
+
+        assertNull(pipeline.first());
+        assertNull(pipeline.last());
+
+        pipeline.addFirst("three", handler3);
+        pipeline.addFirst("two", handler2);
+        pipeline.addFirst("one", handler1);
+
+        assertSame(handler1, pipeline.first());
+        assertSame(pipeline, pipeline.remove(handler1));
+        assertSame(handler2, pipeline.first());
+        assertSame(pipeline, pipeline.remove(handler2));
+        assertSame(handler3, pipeline.first());
+        assertSame(pipeline, pipeline.remove(handler3));
+
+        assertNull(pipeline.first());
+        assertNull(pipeline.last());
+    }
+
+    @Test
+    public void testHandlerCanOptIntoAllEvents() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        Mockito.doAnswer((invocation) -> {
+            ((ProtonEngineHandlerContext) invocation.getArgument(0)).interestMask(ProtonEngineHandlerContext.HANDLER_ALL_EVENTS);
+            return null;
+        }).when(handler).handlerAdded(Mockito.any(EngineHandlerContext.class));
+
+        pipeline.addLast("read-sink", new FrameReadSinkTransportHandler());
+        pipeline.addLast("test", handler);
+        pipeline.addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        pipeline.fireRead(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+        pipeline.fireWrite(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+
+        Mockito.verify(handler).handlerAdded(Mockito.any(EngineHandlerContext.class));
+        Mockito.verify(handler).handleRead(Mockito.any(EngineHandlerContext.class), Mockito.any(HeaderEnvelope.class));
+        Mockito.verify(handler).handleWrite(Mockito.any(EngineHandlerContext.class), Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(handler);
+    }
+
+    @Test
+    public void testHandlerCanOptOutOfReadEvents() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        Mockito.doAnswer((invocation) -> {
+            ((ProtonEngineHandlerContext) invocation.getArgument(0)).interestMask(ProtonEngineHandlerContext.HANDLER_WRITES);
+            return null;
+        }).when(handler).handlerAdded(Mockito.any(EngineHandlerContext.class));
+
+        pipeline.addLast("read-sink", new FrameReadSinkTransportHandler());
+        pipeline.addLast("test", handler);
+        pipeline.addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        pipeline.fireRead(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+
+        Mockito.verify(handler).handlerAdded(Mockito.any(EngineHandlerContext.class));
+        Mockito.verifyNoMoreInteractions(handler);
+    }
+
+    @Test
+    public void testHandlerCanOptOutOfWriteEvents() {
+        ProtonEnginePipeline pipeline = new ProtonEnginePipeline(engine);
+
+        EngineHandler handler = Mockito.mock(EngineHandler.class);
+        Mockito.doAnswer((invocation) -> {
+            ((ProtonEngineHandlerContext) invocation.getArgument(0)).interestMask(ProtonEngineHandlerContext.HANDLER_READS);
+            return null;
+        }).when(handler).handlerAdded(Mockito.any(EngineHandlerContext.class));
+
+        pipeline.addLast("read-sink", new FrameReadSinkTransportHandler());
+        pipeline.addLast("test", handler);
+        pipeline.addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        pipeline.fireWrite(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+
+        Mockito.verify(handler).handlerAdded(Mockito.any(EngineHandlerContext.class));
+        Mockito.verifyNoMoreInteractions(handler);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineTest.java
new file mode 100644
index 0000000..af7ba2f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineTest.java
@@ -0,0 +1,1380 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonByteBuffer;
+import org.apache.qpid.protonj2.engine.AMQPPerformativeEnvelopePool;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.ConnectionState;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineNotStartedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineShutdownException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.MalformedAMQPHeaderException;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.mockito.Mockito;
+
+/**
+ * Test for basic functionality of the ProtonEngine implementation.
+ */
+@Timeout(20)
+public class ProtonEngineTest extends ProtonEngineTestSupport {
+
+    @Test
+    public void testEnginePipelineWriteFailsBeforeStart() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+
+        // Engine cannot accept input bytes until started.
+        assertFalse(engine.isWritable());
+
+        try {
+            engine.pipeline().fireWrite(new ProtonByteBuffer(0), null);
+            fail("Should not be able to write until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        try {
+            engine.pipeline().fireWrite(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+            fail("Should not be able to write until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        try {
+            engine.pipeline().fireWrite(new SASLEnvelope(new SaslInit()));
+            fail("Should not be able to write until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        try {
+            engine.pipeline().fireWrite(AMQPPerformativeEnvelopePool.outgoingEnvelopePool().take(new Open(), 0, null));
+            fail("Should not be able to write until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEnginePipelineReadFailsBeforeStart() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+
+        // Engine cannot accept input bytes until started.
+        assertFalse(engine.isWritable());
+
+        try {
+            engine.pipeline().fireRead(HeaderEnvelope.AMQP_HEADER_ENVELOPE);
+            fail("Should not be able to read data until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        try {
+            engine.pipeline().fireRead(new SASLEnvelope(new SaslInit()));
+            fail("Should not be able to read data until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        try {
+            engine.pipeline().fireRead(AMQPPerformativeEnvelopePool.incomingEnvelopePool().take(new Open(), 0, new ProtonByteBuffer(0)));
+            fail("Should not be able to read data until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        try {
+            engine.pipeline().fireRead(new ProtonByteBuffer(0));
+            fail("Should not be able to write until engine has been started");
+        } catch (EngineNotStartedException error) {
+            // Expected
+        }
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineStart() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+
+        // Engine cannot accept input bytes until started.
+        assertFalse(engine.isWritable());
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        assertFalse(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+
+        // Should be idempotent and return same Connection
+        Connection another = engine.start();
+        assertSame(connection, another);
+
+        // Default engine should start and return a connection immediately
+        assertTrue(engine.isWritable());
+        assertNotNull(connection);
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineShutdown() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+
+        // Engine cannot accept input bytes until started.
+        assertFalse(engine.isWritable());
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        assertTrue(engine.isWritable());
+        assertFalse(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+        assertEquals(EngineState.STARTED, engine.state());
+
+        final AtomicBoolean engineShutdownEventFired = new AtomicBoolean();
+
+        engine.shutdownHandler(theEngine -> engineShutdownEventFired.set(true));
+        engine.shutdown();
+
+        assertFalse(engine.isWritable());
+        assertTrue(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+        assertEquals(EngineState.SHUTDOWN, engine.state());
+        assertTrue(engineShutdownEventFired.get());
+
+        assertNotNull(connection);
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineFailure() {
+        ProtonEngine engine = (ProtonEngine) EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+
+        // Engine cannot accept input bytes until started.
+        assertFalse(engine.isWritable());
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        assertTrue(engine.isWritable());
+        assertFalse(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+        assertEquals(EngineState.STARTED, engine.state());
+
+        engine.engineFailed(new SaslException());
+
+        assertFalse(engine.isWritable());
+        assertFalse(engine.isShutdown());
+        assertTrue(engine.isFailed());
+        assertNotNull(engine.failureCause());
+        assertEquals(EngineState.FAILED, engine.state());
+
+        engine.shutdown();
+
+        assertFalse(engine.isWritable());
+        assertTrue(engine.isShutdown());
+        assertTrue(engine.isFailed());
+        assertNotNull(engine.failureCause());
+        assertEquals(EngineState.SHUTDOWN, engine.state());
+
+        assertNotNull(connection);
+        assertNotNull(failure);
+        assertTrue(failure instanceof SaslException);
+    }
+
+    @Test
+    public void testEngineStartAfterConnectionOpen() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Engine cannot accept input bytes until started.
+        assertFalse(engine.isWritable());
+
+        Connection connection = engine.connection();
+        assertNotNull(connection);
+
+        assertFalse(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+
+        // Should be idempotent and return same Connection
+        Connection another = engine.start();
+        assertSame(connection, another);
+
+        // Default engine should start and return a connection immediately
+        assertTrue(engine.isWritable());
+        assertNotNull(connection);
+        assertNull(failure);
+
+        peer.waitForScriptToComplete();
+    }
+
+    @Test
+    public void testEngineEmitsAMQPHeaderOnConnectionOpen() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+
+        connection.setContainerId("test");
+        connection.open();
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(ConnectionState.ACTIVE, connection.getState());
+        assertEquals(ConnectionState.ACTIVE, connection.getRemoteState());
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTickFailsWhenConnectionNotOpenedNoLocalIdleSet() throws EngineStateException {
+        doTestTickFailsBasedOnState(false, false, false, false);
+    }
+
+    @Test
+    public void testTickFailsWhenConnectionNotOpenedLocalIdleSet() throws EngineStateException {
+        doTestTickFailsBasedOnState(true, false, false, false);
+    }
+
+    @Test
+    public void testTickFailsWhenEngineIsShutdownNoLocalIdleSet() throws EngineStateException {
+        doTestTickFailsBasedOnState(false, true, true, true);
+    }
+
+    @Test
+    public void testTickFailsWhenEngineIsShutdownLocalIdleSet() throws EngineStateException {
+        doTestTickFailsBasedOnState(true, true, true, true);
+    }
+
+    @Test
+    public void testTickFailsWhenEngineIsShutdownButCloseNotCalledNoLocalIdleSet() throws EngineStateException {
+        doTestTickFailsBasedOnState(false, true, false, true);
+    }
+
+    @Test
+    public void testTickFailsWhenEngineIsShutdownButCloseNotCalledLocalIdleSet() throws EngineStateException {
+        doTestTickFailsBasedOnState(true, true, false, true);
+    }
+
+    private void doTestTickFailsBasedOnState(boolean setLocalTimeout, boolean open, boolean close, boolean shutdown) throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        if (setLocalTimeout) {
+            connection.setIdleTimeout(1000);
+        }
+
+        if (open) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            connection.open();
+        }
+
+        if (close) {
+            peer.expectClose().respond();
+            connection.close();
+        }
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+
+        if (shutdown) {
+            engine.shutdown();
+        }
+
+        try {
+            engine.tick(5000);
+            fail("Should not be able to tick an unopened connection");
+        } catch (IllegalStateException | EngineShutdownException error) {
+        }
+    }
+
+    @Test
+    public void testAutoTickFailsWhenConnectionNotOpenedNoLocalIdleSet() throws EngineStateException {
+        doTestAutoTickFailsBasedOnState(false, false, false, false);
+    }
+
+    @Test
+    public void testAutoTickFailsWhenConnectionNotOpenedLocalIdleSet() throws EngineStateException {
+        doTestAutoTickFailsBasedOnState(true, false, false, false);
+    }
+
+    @Test
+    public void testAutoTickFailsWhenEngineShutdownNoLocalIdleSet() throws EngineStateException {
+        doTestAutoTickFailsBasedOnState(false, true, true, true);
+    }
+
+    @Test
+    public void testAutoTickFailsWhenEngineShutdownLocalIdleSet() throws EngineStateException {
+        doTestAutoTickFailsBasedOnState(true, true, true, true);
+    }
+
+    private void doTestAutoTickFailsBasedOnState(boolean setLocalTimeout, boolean open, boolean close, boolean shutdown) throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        if (setLocalTimeout) {
+            connection.setIdleTimeout(1000);
+        }
+
+        if (open) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            peer.expectOpen().respond();
+            connection.open();
+        }
+
+        if (close) {
+            peer.expectClose().respond();
+            connection.close();
+        }
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+
+        if (shutdown) {
+            engine.shutdown();
+        }
+
+        try {
+            engine.tickAuto(Mockito.mock(ScheduledExecutorService.class));
+            fail("Should not be able to tick an unopened connection");
+        } catch (IllegalStateException | EngineShutdownException error) {
+        }
+    }
+
+    @Test
+    public void testTickAutoPreventsDoubleInvocation() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        connection.open();
+
+        engine.tickAuto(Mockito.mock(ScheduledExecutorService.class));
+
+        try {
+            engine.tickAuto(Mockito.mock(ScheduledExecutorService.class));
+            fail("Should not be able call tickAuto more than once.");
+        } catch (IllegalStateException ise) {
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotCallTickAfterTickAutoCalled() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        connection.open();
+
+        engine.tickAuto(Mockito.mock(ScheduledExecutorService.class));
+
+        try {
+            engine.tick(5000);
+            fail("Should not be able call tick after enabling the auto tick feature.");
+        } catch (IllegalStateException ise) {
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTickRemoteTimeout() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        final int remoteTimeout = 4000;
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withIdleTimeOut(nullValue()).respond().withIdleTimeOut(remoteTimeout);
+
+        // Set our local idleTimeout
+        connection.open();
+
+        long deadline = engine.tick(0);
+        assertEquals(2000, deadline, "Expected to be returned a deadline of 2000");  // deadline = 4000 / 2
+
+        deadline = engine.tick(1000);    // Wait for less than the deadline with no data - get the same value
+        assertEquals(2000, deadline, "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(remoteTimeout / 2); // Wait for the deadline - next deadline should be (4000/2)*2
+        assertEquals(4000, deadline, "When the deadline has been reached expected a new deadline to be returned 4000");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.expectBegin();
+        Session session = connection.session().open();
+
+        deadline = engine.tick(3000);
+        assertEquals(5000, deadline, "Writing data resets the deadline");
+        assertEquals(1, peer.getEmptyFrameCount(), "When the deadline is reset tick() shouldn't write an empty frame");
+
+        peer.expectAttach();
+        session.sender("test").open();
+
+        deadline = engine.tick(4000);
+        assertEquals(6000, deadline, "Writing data resets the deadline");
+        assertEquals(1, peer.getEmptyFrameCount(), "When the deadline is reset tick() shouldn't write an empty frame");
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTickLocalTimeout() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        final int localTimeout = 4000;
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withIdleTimeOut(localTimeout).respond();
+
+        // Set our local idleTimeout
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        long deadline = engine.tick(0);
+        assertEquals(4000, deadline, "Expected to be returned a deadline of 4000");
+
+        deadline = engine.tick(1000);    // Wait for less than the deadline with no data - get the same value
+        assertEquals(4000, deadline, "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "Reading data should never result in a frame being written");
+
+        // remote sends an empty frame now
+        peer.remoteEmptyFrame().now();
+
+        deadline = engine.tick(2000);
+        assertEquals(6000, deadline, "Reading data resets the deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "Reading data should never result in a frame being written");
+        assertEquals(ConnectionState.ACTIVE, connection.getState(), "Reading data before the deadline should keep the connection open");
+
+        peer.expectClose().respond();
+
+        deadline = engine.tick(7000);
+        assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testTickWithZeroIdleTimeoutsGivesZeroDeadline() throws EngineStateException {
+        doTickWithNoIdleTimeoutGivesZeroDeadlineTestImpl(true);
+    }
+
+    @Test
+    public void testTickWithNullIdleTimeoutsGivesZeroDeadline() throws EngineStateException {
+        doTickWithNoIdleTimeoutGivesZeroDeadlineTestImpl(false);
+    }
+
+    private void doTickWithNoIdleTimeoutGivesZeroDeadlineTestImpl(boolean useZero) throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        if (useZero) {
+            peer.expectOpen().withIdleTimeOut(nullValue()).respond().withIdleTimeOut(0);
+        } else {
+            peer.expectOpen().withIdleTimeOut(nullValue()).respond();
+        }
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+
+        assertEquals(0, connection.getRemoteIdleTimeout());
+
+        long deadline = engine.tick(0);
+        assertEquals(0, deadline, "Unexpected deadline returned");
+
+        deadline = engine.tick(10);
+        assertEquals(0, deadline, "Unexpected deadline returned");
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTickWithLocalTimeout() throws EngineStateException {
+        // all-positive
+        doTickWithLocalTimeoutTestImpl(4000, 10000, 14000, 18000, 22000);
+
+        // all-negative
+        doTickWithLocalTimeoutTestImpl(2000, -100000, -98000, -96000, -94000);
+
+        // negative to positive missing 0
+        doTickWithLocalTimeoutTestImpl(500, -950, -450, 50, 550);
+
+        // negative to positive striking 0
+        doTickWithLocalTimeoutTestImpl(3000, -6000, -3000, 1, 3001);
+    }
+
+    private void doTickWithLocalTimeoutTestImpl(int localTimeout, long tick1, long expectedDeadline1, long expectedDeadline2, long expectedDeadline3) throws EngineStateException {
+        this.failure = null;
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withIdleTimeOut(localTimeout).respond();
+
+        // Set our local idleTimeout
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+
+        long deadline = engine.tick(tick1);
+        assertEquals(expectedDeadline1, deadline, "Unexpected deadline returned");
+
+        // Wait for less time than the deadline with no data - get the same value
+        long interimTick = tick1 + 10;
+        assertTrue(interimTick < expectedDeadline1);
+        assertEquals(expectedDeadline1, engine.tick(interimTick), "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(1, peer.getPerformativeCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+        assertNull(failure);
+
+        peer.remoteEmptyFrame().now();
+
+        deadline = engine.tick(expectedDeadline1);
+        assertEquals(expectedDeadline2, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(1, peer.getPerformativeCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+        assertNull(failure);
+
+        peer.remoteEmptyFrame().now();
+
+        deadline = engine.tick(expectedDeadline2);
+        assertEquals(expectedDeadline3, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(1, peer.getPerformativeCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+        assertNull(failure);
+
+        peer.expectClose().withError(notNullValue()).respond();
+
+        assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+        engine.tick(expectedDeadline3); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
+        assertEquals(2, peer.getPerformativeCount(), "tick() should have written data");
+        assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+
+        peer.waitForScriptToComplete();
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testTickWithRemoteTimeout() throws EngineStateException {
+        // all-positive
+        doTickWithRemoteTimeoutTestImpl(4000, 10000, 14000, 18000, 22000);
+
+        // all-negative
+        doTickWithRemoteTimeoutTestImpl(2000, -100000, -98000, -96000, -94000);
+
+        // negative to positive missing 0
+        doTickWithRemoteTimeoutTestImpl(500, -950, -450, 50, 550);
+
+        // negative to positive striking 0
+        doTickWithRemoteTimeoutTestImpl(3000, -6000, -3000, 1, 3001);
+    }
+
+    private void doTickWithRemoteTimeoutTestImpl(int remoteTimeoutHalf, long tick1, long expectedDeadline1, long expectedDeadline2, long expectedDeadline3) throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
+        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
+        peer.expectOpen().respond().withIdleTimeOut(remoteTimeoutHalf * 2);
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+
+        long deadline = engine.tick(tick1);
+        assertEquals(expectedDeadline1, deadline, "Unexpected deadline returned");
+
+        // Wait for less time than the deadline with no data - get the same value
+        long interimTick = tick1 + 10;
+        assertTrue(interimTick < expectedDeadline1);
+        assertEquals(expectedDeadline1, engine.tick(interimTick), "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(1, peer.getPerformativeCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+        assertEquals(0, peer.getEmptyFrameCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(expectedDeadline1);
+        assertEquals(expectedDeadline2, deadline, "When the deadline has been reached expected a new remote deadline to be returned");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.expectBegin();
+
+        // Do some actual work, create real traffic, removing the need to send empty frame to satisfy idle-timeout
+        connection.session().open();
+
+        assertEquals(2, peer.getPerformativeCount(), "session open should have written data");
+
+        deadline = engine.tick(expectedDeadline2);
+        assertEquals(expectedDeadline3, deadline, "When the deadline has been reached expected a new remote deadline to be returned");
+        assertEquals(2, peer.getPerformativeCount(), "tick() should not have written data as there was actual activity");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should not have written data as there was actual activity");
+
+        peer.expectEmptyFrame();
+
+        engine.tick(expectedDeadline3);
+        assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTickWithBothTimeouts() throws EngineStateException {
+        // all-positive
+        doTickWithBothTimeoutsTestImpl(true, 5000, 2000, 10000, 12000, 14000, 15000);
+        doTickWithBothTimeoutsTestImpl(false, 5000, 2000, 10000, 12000, 14000, 15000);
+
+        // all-negative
+        doTickWithBothTimeoutsTestImpl(true, 10000, 4000, -100000, -96000, -92000, -90000);
+        doTickWithBothTimeoutsTestImpl(false, 10000, 4000, -100000, -96000, -92000, -90000);
+
+        // negative to positive missing 0
+        doTickWithBothTimeoutsTestImpl(true, 500, 200, -450, -250, -50, 50);
+        doTickWithBothTimeoutsTestImpl(false, 500, 200, -450, -250, -50, 50);
+
+        // negative to positive striking 0 with local deadline
+        doTickWithBothTimeoutsTestImpl(true, 500, 200, -500, -300, -100, 1);
+        doTickWithBothTimeoutsTestImpl(false, 500, 200, -500, -300, -100, 1);
+
+        // negative to positive striking 0 with remote deadline
+        doTickWithBothTimeoutsTestImpl(true, 500, 200, -200, 1, 201, 300);
+        doTickWithBothTimeoutsTestImpl(false, 500, 200, -200, 1, 201, 300);
+    }
+
+    private void doTickWithBothTimeoutsTestImpl(boolean allowLocalTimeout, int localTimeout, int remoteTimeoutHalf, long tick1,
+                                                long expectedDeadline1, long expectedDeadline2, long expectedDeadline3) throws EngineStateException {
+
+        this.failure = null;
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
+        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
+        peer.expectOpen().respond().withIdleTimeOut(remoteTimeoutHalf * 2);
+
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        long deadline = engine.tick(tick1);
+        assertEquals(expectedDeadline1, deadline, "Unexpected deadline returned");
+
+        // Wait for less time than the deadline with no data - get the same value
+        long interimTick = tick1 + 10;
+        assertTrue(interimTick < expectedDeadline1);
+        assertEquals(expectedDeadline1, engine.tick(interimTick), "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(expectedDeadline1);
+        assertEquals(expectedDeadline2, deadline, "When the deadline has been reached expected a new remote deadline to be returned");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(expectedDeadline2);
+        assertEquals(expectedDeadline3, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.waitForScriptToComplete();
+
+        if (allowLocalTimeout) {
+            peer.expectClose().respond();
+
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+            engine.tick(expectedDeadline3); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
+            assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+            assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data but not an empty frame");
+
+            peer.waitForScriptToComplete();
+            assertNotNull(failure);
+        } else {
+            peer.remoteEmptyFrame().now();
+
+            deadline = engine.tick(expectedDeadline3);
+            assertEquals(expectedDeadline2 + (remoteTimeoutHalf), deadline, "Receiving data should have reset the deadline (to the next remote one)");
+            assertEquals(2, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+
+            peer.waitForScriptToComplete();
+            assertNull(failure);
+        }
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemote() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteTestImpl(false);
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteWithLocalTimeout() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteTestImpl(true);
+    }
+
+    private void doTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteTestImpl(boolean allowLocalTimeout) throws EngineStateException {
+        int localTimeout = 5000;
+        int remoteTimeoutHalf = 2000;
+        assertTrue(remoteTimeoutHalf < localTimeout);
+
+        long offset = 2500;
+        assertTrue(offset < localTimeout);
+        assertTrue(offset > remoteTimeoutHalf);
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
+        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
+        peer.expectOpen().respond().withIdleTimeOut(remoteTimeoutHalf * 2);
+
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        long deadline = engine.tick(Long.MAX_VALUE - offset);
+        assertEquals(Long.MAX_VALUE - offset + remoteTimeoutHalf, deadline, "Unexpected deadline returned");
+
+        deadline = engine.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
+        assertEquals(Long.MAX_VALUE -offset + remoteTimeoutHalf, deadline, "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(Long.MAX_VALUE -offset + remoteTimeoutHalf); // Wait for the deadline - next deadline should be previous + remoteTimeoutHalf;
+        assertEquals(Long.MIN_VALUE + (2* remoteTimeoutHalf) - offset -1, deadline, "When the deadline has been reached expected a new remote deadline to be returned");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(Long.MIN_VALUE + (2* remoteTimeoutHalf) - offset -1); // Wait for the deadline - next deadline should be orig + localTimeout;
+        assertEquals(Long.MIN_VALUE + (localTimeout - offset) -1, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.waitForScriptToComplete();
+
+        if (allowLocalTimeout) {
+            peer.expectClose().respond();
+
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+            engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
+            assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+            assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data but not an empty frame");
+
+            peer.waitForScriptToComplete();
+            assertNotNull(failure);
+        } else {
+            peer.remoteEmptyFrame().now();
+
+            deadline = engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline - next deadline should be orig + 3*remoteTimeoutHalf;
+            assertEquals(Long.MIN_VALUE + (3* remoteTimeoutHalf) - offset -1, deadline, "Receiving data should have reset the deadline (to the remote one)");
+            assertEquals(2, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+
+            peer.waitForScriptToComplete();
+            assertNull(failure);
+        }
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocal() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalTestImpl(false);
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalWithLocalTimeout() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalTestImpl(true);
+    }
+
+    private void doTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalTestImpl(boolean allowLocalTimeout) throws EngineStateException {
+        int localTimeout = 2000;
+        int remoteTimeoutHalf = 5000;
+        assertTrue(localTimeout < remoteTimeoutHalf);
+
+        long offset = 2500;
+        assertTrue(offset > localTimeout);
+        assertTrue(offset < remoteTimeoutHalf);
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
+        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
+        peer.expectOpen().respond().withIdleTimeOut(remoteTimeoutHalf * 2);
+
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        long deadline = engine.tick(Long.MAX_VALUE - offset);
+        assertEquals(Long.MAX_VALUE - offset + localTimeout, deadline, "Unexpected deadline returned");
+
+        deadline = engine.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
+        assertEquals(Long.MAX_VALUE - offset + localTimeout, deadline, "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+
+        // Receive Empty frame to satisfy local deadline
+        peer.remoteEmptyFrame().now();
+
+        deadline = engine.tick(Long.MAX_VALUE - offset + localTimeout); // Wait for the deadline - next deadline should be orig + 2* localTimeout;
+        assertEquals(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(0, peer.getEmptyFrameCount(), "tick() should not have written data");
+
+        peer.waitForScriptToComplete();
+
+        if (allowLocalTimeout) {
+            peer.expectClose().respond();
+
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+            engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
+            assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+            assertEquals(0, peer.getEmptyFrameCount(), "tick() should have written data but not an empty frame");
+
+            peer.waitForScriptToComplete();
+            assertNotNull(failure);
+        } else {
+            // Receive Empty frame to satisfy local deadline
+            peer.remoteEmptyFrame().now();
+
+            deadline = engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline - next deadline should be orig + remoteTimeoutHalf;
+            assertEquals(Long.MIN_VALUE + remoteTimeoutHalf - offset -1, deadline, "Receiving data should have reset the deadline (to the remote one)");
+            assertEquals(0, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+
+            peer.expectEmptyFrame();
+
+            deadline = engine.tick(Long.MIN_VALUE + remoteTimeoutHalf - offset -1); // Wait for the deadline - next deadline should be orig + 3* localTimeout;
+            assertEquals(Long.MIN_VALUE + (3* localTimeout) - offset -1, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+            assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written an empty frame");
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+
+            peer.waitForScriptToComplete();
+            assertNull(failure);
+        }
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirst() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstTestImpl(false);
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstWithLocalTimeout() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstTestImpl(true);
+    }
+
+    private void doTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstTestImpl(boolean allowLocalTimeout) throws EngineStateException {
+        int localTimeout = 2000;
+        int remoteTimeoutHalf = 2500;
+        assertTrue(localTimeout < remoteTimeoutHalf);
+
+        long offset = 500;
+        assertTrue(offset < localTimeout);
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
+        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
+        peer.expectOpen().respond().withIdleTimeOut(remoteTimeoutHalf * 2);
+
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        long deadline = engine.tick(Long.MAX_VALUE - offset);
+        assertEquals(Long.MIN_VALUE + (localTimeout - offset) -1, deadline, "Unexpected deadline returned");
+
+        deadline = engine.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
+        assertEquals(Long.MIN_VALUE + (localTimeout - offset) -1, deadline, "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+
+        // Receive Empty frame to satisfy local deadline
+        peer.remoteEmptyFrame().now();
+
+        deadline = engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline - next deadline should be orig + remoteTimeoutHalf;
+        assertEquals(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1, deadline, "When the deadline has been reached expected a new remote deadline to be returned");
+        assertEquals(0, peer.getEmptyFrameCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1); // Wait for the deadline - next deadline should be orig + 2* localTimeout;
+        assertEquals(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.waitForScriptToComplete();
+
+        if (allowLocalTimeout) {
+            peer.expectClose().respond();
+
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+            engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
+            assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+            assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data but not an empty frame");
+
+            peer.waitForScriptToComplete();
+            assertNotNull(failure);
+        } else {
+            // Receive Empty frame to satisfy local deadline
+            peer.remoteEmptyFrame().now();
+
+            deadline = engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline - next deadline should be orig + 2*remoteTimeoutHalf;
+            assertEquals(Long.MIN_VALUE + (2* remoteTimeoutHalf) - offset -1, deadline, "Receiving data should have reset the deadline (to the remote one)");
+            assertEquals(1, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+
+            peer.waitForScriptToComplete();
+            assertNull(failure);
+        }
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirst() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstTestImpl(false);
+    }
+
+    @Test
+    public void testTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstWithLocalTimeout() throws EngineStateException {
+        doTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstTestImpl(true);
+    }
+
+    private void doTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstTestImpl(boolean allowLocalTimeout) throws EngineStateException {
+        int localTimeout = 5000;
+        int remoteTimeoutHalf = 2000;
+        assertTrue(remoteTimeoutHalf < localTimeout);
+
+        long offset = 500;
+        assertTrue(offset < remoteTimeoutHalf);
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
+        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
+        peer.expectOpen().respond().withIdleTimeOut(remoteTimeoutHalf * 2);
+
+        connection.setIdleTimeout(localTimeout);
+        connection.open();
+
+        long deadline = engine.tick(Long.MAX_VALUE - offset);
+        assertEquals(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1, deadline, "Unexpected deadline returned");
+
+        deadline = engine.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
+        assertEquals(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1, deadline, "When the deadline hasn't been reached tick() should return the previous deadline");
+        assertEquals(0, peer.getEmptyFrameCount(), "When the deadline hasn't been reached tick() shouldn't write data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1); // Wait for the deadline - next deadline should be previous + remoteTimeoutHalf;
+        assertEquals(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1 + remoteTimeoutHalf, deadline, "When the deadline has been reached expected a new remote deadline to be returned");
+        assertEquals(1, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.expectEmptyFrame();
+
+        deadline = engine.tick(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1 + remoteTimeoutHalf); // Wait for the deadline - next deadline should be orig + localTimeout;
+        assertEquals(Long.MIN_VALUE + (localTimeout - offset) -1, deadline, "When the deadline has been reached expected a new local deadline to be returned");
+        assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data");
+
+        peer.waitForScriptToComplete();
+
+        if (allowLocalTimeout) {
+            peer.expectClose().respond();
+
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+            engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
+            assertEquals(ConnectionState.CLOSED, connection.getState(), "Calling tick() after the deadline should result in the connection being closed");
+            assertEquals(2, peer.getEmptyFrameCount(), "tick() should have written data but not an empty frame");
+
+            peer.waitForScriptToComplete();
+            assertNotNull(failure);
+        } else {
+            // Receive Empty frame to satisfy local deadline
+            peer.remoteEmptyFrame().now();
+
+            deadline = engine.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline - next deadline should be orig + 3*remoteTimeoutHalf;
+            assertEquals(Long.MIN_VALUE + (3* remoteTimeoutHalf) - offset -1, deadline, "Receiving data should have reset the deadline (to the remote one)");
+            assertEquals(2, peer.getEmptyFrameCount(), "tick() shouldn't have written data");
+            assertEquals(ConnectionState.ACTIVE, connection.getState(), "Connection should be active");
+
+            peer.waitForScriptToComplete();
+            assertNull(failure);
+        }
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte1() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'a', 'M', 'Q', 'P', 0, 1, 0, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte2() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'm', 'Q', 'P', 0, 1, 0, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte3() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'M', 'q', 'P', 0, 1, 0, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte4() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'M', 'Q', 'p', 0, 1, 0, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte5() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'M', 'Q', 'P', 99, 1, 0, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte6() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'M', 'Q', 'P', 0, 99, 0, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte7() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'M', 'Q', 'P', 0, 1, 99, 0 });
+    }
+
+    @Test
+    public void testEngineFailsWithMeaningfulErrorOnNonAMQPHeaderResponseBadByte8() throws EngineStateException {
+        doTestEngineFailsWithMalformedHeaderException(new byte[] { 'A', 'M', 'Q', 'P', 0, 1, 0, 99 });
+    }
+
+    private final void doTestEngineFailsWithMalformedHeaderException(byte[] headerBytes) {
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithBytes(headerBytes);
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+        connection.negotiate();
+
+        peer.waitForScriptToCompleteIgnoreErrors();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof MalformedAMQPHeaderException);
+    }
+
+    @Test
+    public void testEngineConfiguresDefaultMaxFrameSizeLimits() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Connection connection = engine.start();
+        assertNotNull(connection);
+        ProtonEngineConfiguration configuration = (ProtonEngineConfiguration) engine.configuration();
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE).respond();
+
+        connection.open();
+
+        assertEquals(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE, configuration.getOutboundMaxFrameSize());
+        assertEquals(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE, configuration.getInboundMaxFrameSize());
+
+        // Default engine should start and return a connection immediately
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineConfiguresSpecifiedMaxFrameSizeLimitsMatchesDefaultMinMax() {
+        doTestEngineConfiguresSpecifiedFrameSizeLimits(512, 512);
+    }
+
+    @Test
+    public void testEngineConfiguresSpecifiedMaxFrameSizeLimitsRemoteLargerThanLocal() {
+        doTestEngineConfiguresSpecifiedFrameSizeLimits(1024, 1025);
+    }
+
+    @Test
+    public void testEngineConfiguresSpecifiedMaxFrameSizeLimitsRemoteSmallerThanLocal() {
+        doTestEngineConfiguresSpecifiedFrameSizeLimits(1024, 1023);
+    }
+
+    @Test
+    public void testEngineConfiguresSpecifiedMaxFrameSizeLimitsGreaterThanDefaultValues() {
+        doTestEngineConfiguresSpecifiedFrameSizeLimits(
+            ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE + 32, ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE + 64);
+    }
+
+    @Test
+    public void testEngineConfiguresRemoteMaxFrameSizeSetToMaxUnsignedLong() {
+        doTestEngineConfiguresSpecifiedFrameSizeLimits(
+            Integer.MAX_VALUE, UnsignedInteger.MAX_VALUE.intValue());
+    }
+
+    private void doTestEngineConfiguresSpecifiedFrameSizeLimits(int localValue, int remoteResponse) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Connection connection = engine.start();
+        assertNotNull(connection);
+        ProtonEngineConfiguration configuration = (ProtonEngineConfiguration) engine.configuration();
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(Integer.toUnsignedLong(localValue))
+                         .respond()
+                         .withMaxFrameSize(Integer.toUnsignedLong(remoteResponse));
+
+        connection.setMaxFrameSize(Integer.toUnsignedLong(localValue));
+        connection.open();
+
+        if (localValue > 0) {
+            assertEquals(localValue, configuration.getInboundMaxFrameSize());
+        } else {
+            assertEquals(Integer.MAX_VALUE, configuration.getInboundMaxFrameSize());
+        }
+
+        if (remoteResponse > localValue) {
+            assertEquals(localValue, configuration.getOutboundMaxFrameSize());
+        } else {
+            if (remoteResponse > 0) {
+                assertEquals(remoteResponse, configuration.getOutboundMaxFrameSize());
+            } else {
+                assertEquals(Integer.MAX_VALUE, configuration.getOutboundMaxFrameSize());
+            }
+        }
+
+        assertEquals(UnsignedInteger.toUnsignedLong(localValue), connection.getMaxFrameSize());
+        assertEquals(UnsignedInteger.toUnsignedLong(remoteResponse), connection.getRemoteMaxFrameSize());
+
+        // Default engine should start and return a connection immediately
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineErrorsOnLocalMaxFrameSizeLargerThanImposedLimit() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        assertThrows(IllegalArgumentException.class, () -> connection.setMaxFrameSize(UnsignedInteger.MAX_VALUE.longValue()));
+    }
+
+    @Test
+    public void testEngineShutdownHandlerThrowsIsIngoredAndShutdownCompletes() {
+        ProtonEngine engine = (ProtonEngine) EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+
+        engine.shutdownHandler((theEngine) -> {
+            throw new RuntimeException();
+        });
+
+        Connection connection = engine.start();
+        assertNotNull(connection);
+
+        assertTrue(engine.isWritable());
+        assertTrue(engine.isRunning());
+        assertFalse(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+        assertEquals(EngineState.STARTED, engine.state());
+
+        try {
+            engine.shutdown();
+            fail("User event handler throw wasn't propagated");
+        } catch (RuntimeException expected) {
+            // Expected
+        }
+
+        assertFalse(engine.isWritable());
+        assertFalse(engine.isRunning());
+        assertTrue(engine.isShutdown());
+        assertFalse(engine.isFailed());
+        assertNull(engine.failureCause());
+        assertEquals(EngineState.SHUTDOWN, engine.state());
+
+        // should not perform any additional work.
+        engine.shutdown();
+
+        assertNotNull(connection);
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEnginePipelineProtectsFromExternalUserMischief() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.connection().open();
+
+        peer.waitForScriptToComplete();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+
+        engine.start();
+
+        assertTrue(engine.isWritable());
+        assertNotNull(connection);
+        assertNull(failure);
+
+        assertThrows(IllegalAccessError.class, () -> engine.pipeline().fireEngineStarting());
+        assertThrows(IllegalAccessError.class, () -> engine.pipeline().fireEngineStateChanged());
+        assertThrows(IllegalAccessError.class, () -> engine.pipeline().fireFailed(new EngineFailedException(null)));
+
+        engine.shutdown();
+
+        assertThrows(EngineShutdownException.class, () -> engine.pipeline().first());
+        assertThrows(EngineShutdownException.class, () -> engine.pipeline().last());
+        assertThrows(EngineShutdownException.class, () -> engine.pipeline().firstContext());
+        assertThrows(EngineShutdownException.class, () -> engine.pipeline().lastContext());
+
+        peer.waitForScriptToComplete();
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineTestSupport.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineTestSupport.java
new file mode 100644
index 0000000..9bfe912
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonEngineTestSupport.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Queue;
+import java.util.Random;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.codec.Encoder;
+import org.apache.qpid.protonj2.codec.EncoderState;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+
+/**
+ * Base class for Proton Engine and its components.
+ */
+public abstract class ProtonEngineTestSupport {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonEngineTestSupport.class);
+
+    protected ArrayList<ProtonBuffer> engineWrites = new ArrayList<>();
+
+    protected final Decoder decoder = CodecFactory.getDefaultDecoder();
+    protected final DecoderState decoderState = decoder.newDecoderState();
+
+    protected final Encoder encoder = CodecFactory.getDefaultEncoder();
+    protected final EncoderState encoderState = encoder.newEncoderState();
+
+    protected Throwable failure;
+    protected String testName;
+
+    @BeforeEach
+    public void setUp(TestInfo testInfo) {
+        testName = testInfo.getDisplayName();
+        LOG.info("========== start " + testInfo.getDisplayName() + " ==========");
+    }
+
+    @AfterEach
+    public void tearDown(TestInfo testInfo) {
+        engineWrites.clear();
+        decoderState.reset();
+        encoderState.reset();
+
+        failure = null;
+
+        LOG.info("========== tearDown " + testInfo.getDisplayName() + " ==========");
+    }
+
+    protected ProtonBuffer wrapInFrame(Object input, int channel) {
+        final int FRAME_START_BYTE = 0;
+        final int FRAME_DOFF_BYTE = 4;
+        final int FRAME_DOFF_SIZE = 2;
+        final int FRAME_TYPE_BYTE = 5;
+        final int FRAME_CHANNEL_BYTE = 6;
+
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(512);
+
+        buffer.writeLong(0); // Reserve header space
+
+        try {
+            encoder.writeObject(buffer, encoderState, input);
+        } finally {
+            encoderState.reset();
+        }
+
+        buffer.setInt(FRAME_START_BYTE, buffer.getReadableBytes());
+        buffer.setByte(FRAME_DOFF_BYTE, FRAME_DOFF_SIZE);
+        buffer.setByte(FRAME_TYPE_BYTE, 0);
+        buffer.setShort(FRAME_CHANNEL_BYTE, channel);
+
+        return buffer;
+    }
+
+    @SuppressWarnings({ "unchecked", "unused" })
+    protected <E> E unwrapFrame(ProtonBuffer buffer, Class<E> typeClass) throws IOException {
+        int frameSize = buffer.readInt();
+        int dataOffset = (buffer.readByte() << 2) & 0x3FF;
+        int type = buffer.readByte() & 0xFF;
+        short channel = buffer.readShort();
+        if (dataOffset != 8) {
+            buffer.setReadIndex(buffer.getReadIndex() + dataOffset - 8);
+        }
+
+        final int frameBodySize = frameSize - dataOffset;
+
+        ProtonBuffer payload = null;
+        Object val = null;
+
+        if (frameBodySize > 0) {
+            try {
+                val = decoder.readObject(buffer, decoderState);
+            } finally {
+                decoderState.reset();
+            }
+        } else {
+            val = null;
+        }
+
+        return (E) val;
+    }
+
+    protected static ProtonBuffer createContentBuffer(int length) {
+        Random rand = new Random(System.currentTimeMillis());
+
+        byte[] payload = new byte[length];
+        for (int i = 0; i < length; i++) {
+            payload[i] = (byte) (64 + 1 + rand.nextInt(9));
+        }
+
+        return ProtonByteBufferAllocator.DEFAULT.wrap(payload).setIndex(0, length);
+    }
+
+    protected ProtonTestConnector createTestPeer(Engine engine) {
+        ProtonTestConnector peer = new ProtonTestConnector(buffer -> {
+            engine.accept(ProtonByteBufferAllocator.DEFAULT.wrap(buffer));
+        });
+        engine.outputConsumer(buffer -> {
+            peer.accept(buffer.toByteBuffer());
+        });
+
+        return peer;
+    }
+
+    protected ProtonTestConnector createTestPeer(Engine engine, Queue<Runnable> asyncIOCallback) {
+        ProtonTestConnector peer = new ProtonTestConnector(buffer -> {
+            engine.accept(ProtonByteBufferAllocator.DEFAULT.wrap(buffer));
+        });
+        engine.outputHandler((buffer, callback) -> {
+            if (callback != null) {
+                asyncIOCallback.offer(callback);
+            }
+            peer.accept(buffer.toByteBuffer());
+        });
+
+        return peer;
+    }
+
+    protected String getTestName() {
+        return getClass().getSimpleName() + "." + testName;
+    }
+
+    protected byte[] createEncodedMessage(Section<Object> body) {
+        Encoder encoder = CodecFactory.getEncoder();
+        ProtonBuffer buffer = new ProtonByteBufferAllocator().allocate();
+        encoder.writeObject(buffer, encoder.newEncoderState(), body);
+        byte[] result = new byte[buffer.getReadableBytes()];
+        buffer.readBytes(result);
+        return result;
+    }
+
+    protected byte[] createEncodedMessage(Section<?>... body) {
+        Encoder encoder = CodecFactory.getEncoder();
+        ProtonBuffer buffer = new ProtonByteBufferAllocator().allocate();
+        for (Section<?> section : body) {
+            encoder.writeObject(buffer, encoder.newEncoderState(), section);
+        }
+        byte[] result = new byte[buffer.getReadableBytes()];
+        buffer.readBytes(result);
+        return result;
+    }
+
+    protected byte[] createEncodedMessage(Data... body) {
+        Encoder encoder = CodecFactory.getEncoder();
+        ProtonBuffer buffer = new ProtonByteBufferAllocator().allocate();
+        for (Data data : body) {
+            encoder.writeObject(buffer, encoder.newEncoderState(), data);
+        }
+        byte[] result = new byte[buffer.getReadableBytes()];
+        buffer.readBytes(result);
+        return result;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameDecodingHandlerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameDecodingHandlerTest.java
new file mode 100644
index 0000000..a41052d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameDecodingHandlerTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.List;
+
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.EmptyEnvelope;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.util.FrameReadSinkTransportHandler;
+import org.apache.qpid.protonj2.engine.util.FrameRecordingTransportHandler;
+import org.apache.qpid.protonj2.engine.util.FrameWriteSinkTransportHandler;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+public class ProtonFrameDecodingHandlerTest {
+
+    private FrameRecordingTransportHandler testHandler;
+
+    @BeforeEach
+    public void setUp() {
+        testHandler = new FrameRecordingTransportHandler();
+    }
+
+    @Test
+    public void testDecodeValidHeaderTriggersHeaderRead() {
+        Engine engine = createEngine();
+
+        engine.start();
+
+        // Check for Header processing
+        engine.pipeline().fireRead(AMQPHeader.getAMQPHeader().getBuffer());
+
+        Object frame = testHandler.getFramesRead().get(0);
+        assertTrue(frame instanceof HeaderEnvelope);
+        HeaderEnvelope header = (HeaderEnvelope) frame;
+        assertEquals(AMQPHeader.getAMQPHeader(), header.getBody());
+    }
+
+    @Test
+    public void testReadValidHeaderInSingleByteChunks() throws Exception {
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 'A' }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 'M' }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 'Q' }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 'P' }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0 }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 1 }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0 }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0 }));
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    @Test
+    public void testReadValidHeaderInSplitChunks() throws Exception {
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 'A', 'M', 'Q', 'P' }));
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 0, 1, 0, 0 }));
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    @Test
+    public void testDecodeValidSaslHeaderTriggersHeaderRead() {
+        Engine engine = createEngine();
+
+        engine.start();
+
+        // Check for Header processing
+        engine.pipeline().fireRead(AMQPHeader.getSASLHeader().getBuffer());
+
+        Object frame = testHandler.getFramesRead().get(0);
+        assertTrue(frame instanceof HeaderEnvelope);
+        HeaderEnvelope header = (HeaderEnvelope) frame;
+        assertEquals(AMQPHeader.getSASLHeader(), header.getBody());
+    }
+
+    @Test
+    public void testInvalidHeaderBytesTriggersError() {
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        try {
+            handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 'S' }));
+            fail("Handler should throw error on invalid input");
+        } catch (Throwable error) {
+            // Expected
+        }
+
+        // Verify that the parser accepts no new input once in error state.
+        Mockito.clearInvocations(context);
+        try {
+            handler.handleRead(context, AMQPHeader.getSASLHeader().getBuffer());
+            fail("Handler should throw error on additional input");
+        } catch (Throwable error) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testDecodeEmptyOpenEncodedFrame() throws Exception {
+        // Frame data for: Open
+        //   Open{ containerId="", hostname='null', maxFrameSize=4294967295, channelMax=65535,
+        //         idleTimeOut=null, outgoingLocales=null, incomingLocales=null, offeredCapabilities=null,
+        //         desiredCapabilities=null, properties=null}
+        final byte[] emptyOpen = new byte[] {0, 0, 0, 16, 2, 0, 0, 0, 0, 83, 16, -64, 3, 1, -95, 0};
+
+        ArgumentCaptor<IncomingAMQPEnvelope> argument = ArgumentCaptor.forClass(IncomingAMQPEnvelope.class);
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(emptyOpen));
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verify(context).fireRead(argument.capture());
+        Mockito.verifyNoMoreInteractions(context);
+
+        assertNotNull(argument.getValue());
+        assertTrue(argument.getValue().getBody() instanceof Open);
+
+        Open decoded = (Open) argument.getValue().getBody();
+
+        assertTrue(decoded.hasContainerId());  // Defaults to empty string from proton-j
+        assertFalse(decoded.hasHostname());
+        assertFalse(decoded.hasMaxFrameSize());
+        assertFalse(decoded.hasChannelMax());
+        assertFalse(decoded.hasIdleTimeout());
+        assertFalse(decoded.hasOutgoingLocales());
+        assertFalse(decoded.hasIncomingLocales());
+        assertFalse(decoded.hasOfferedCapabilites());
+        assertFalse(decoded.hasDesiredCapabilites());
+        assertFalse(decoded.hasProperties());
+    }
+
+    @Test
+    public void testDecodeSimpleOpenEncodedFrame() throws Exception {
+        // Frame data for: Open
+        //   Open{ containerId='container', hostname='localhost', maxFrameSize=16384, channelMax=65535,
+        //         idleTimeOut=30000, outgoingLocales=null, incomingLocales=null, offeredCapabilities=null,
+        //         desiredCapabilities=null, properties=null}
+        final byte[] basicOpen = new byte[] {0, 0, 0, 49, 2, 0, 0, 0, 0, 83, 16, -64, 36, 5, -95, 9, 99, 111,
+                                             110, 116, 97, 105, 110, 101, 114, -95, 9, 108, 111, 99, 97, 108,
+                                             104, 111, 115, 116, 112, 0, 0, 64, 0, 96, -1, -1, 112, 0, 0, 117, 48};
+        ArgumentCaptor<IncomingAMQPEnvelope> argument = ArgumentCaptor.forClass(IncomingAMQPEnvelope.class);
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(basicOpen));
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verify(context).fireRead(argument.capture());
+        Mockito.verifyNoMoreInteractions(context);
+
+        assertNotNull(argument.getValue());
+        assertTrue(argument.getValue().getBody() instanceof Open);
+
+        Open decoded = (Open) argument.getValue().getBody();
+
+        assertTrue(decoded.hasContainerId());
+        assertEquals("container", decoded.getContainerId());
+        assertTrue(decoded.hasHostname());
+        assertEquals("localhost", decoded.getHostname());
+        assertTrue(decoded.hasMaxFrameSize());
+        assertEquals(16384, decoded.getMaxFrameSize());
+        assertTrue(decoded.hasChannelMax());
+        assertTrue(decoded.hasIdleTimeout());
+        assertEquals(30000, decoded.getIdleTimeout());
+        assertFalse(decoded.hasOutgoingLocales());
+        assertFalse(decoded.hasIncomingLocales());
+        assertFalse(decoded.hasOfferedCapabilites());
+        assertFalse(decoded.hasDesiredCapabilites());
+        assertFalse(decoded.hasProperties());
+    }
+
+    @Test
+    public void testDecodePipelinedHeaderAndOpenEncodedFrame() throws Exception {
+        // Frame data for: Open
+        //   Open{ containerId='container', hostname='localhost', maxFrameSize=16384, channelMax=65535,
+        //         idleTimeOut=30000, outgoingLocales=null, incomingLocales=null, offeredCapabilities=null,
+        //         desiredCapabilities=null, properties=null}
+        final byte[] basicOpen = new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0, // HEADER
+                                             0, 0, 0, 49, 2, 0, 0, 0, 0, 83, 16, -64, 36, 5, -95, 9, 99, 111,
+                                             110, 116, 97, 105, 110, 101, 114, -95, 9, 108, 111, 99, 97, 108,
+                                             104, 111, 115, 116, 112, 0, 0, 64, 0, 96, -1, -1, 112, 0, 0, 117, 48};
+        ArgumentCaptor<IncomingAMQPEnvelope> argument = ArgumentCaptor.forClass(IncomingAMQPEnvelope.class);
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(basicOpen));
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verify(context).fireRead(argument.capture());
+        Mockito.verifyNoMoreInteractions(context);
+
+        assertNotNull(argument.getValue());
+        assertTrue(argument.getValue().getBody() instanceof Open);
+
+        Open decoded = (Open) argument.getValue().getBody();
+
+        assertTrue(decoded.hasContainerId());
+        assertEquals("container", decoded.getContainerId());
+        assertTrue(decoded.hasHostname());
+        assertEquals("localhost", decoded.getHostname());
+        assertTrue(decoded.hasMaxFrameSize());
+        assertEquals(16384, decoded.getMaxFrameSize());
+        assertTrue(decoded.hasChannelMax());
+        assertTrue(decoded.hasIdleTimeout());
+        assertEquals(30000, decoded.getIdleTimeout());
+        assertFalse(decoded.hasOutgoingLocales());
+        assertFalse(decoded.hasIncomingLocales());
+        assertFalse(decoded.hasOfferedCapabilites());
+        assertFalse(decoded.hasDesiredCapabilites());
+        assertFalse(decoded.hasProperties());
+    }
+
+    /*
+     * Test that empty frames, as used for heartbeating, decode as expected.
+     */
+    @Test
+    public void testDecodeEmptyFrame() throws Exception {
+        // http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#doc-idp124752
+        // Description: '8byte sized' empty AMQP frame
+        byte[] emptyFrame = new byte[] { (byte) 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00 };
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(emptyFrame));
+
+        ArgumentCaptor<IncomingAMQPEnvelope> argument = ArgumentCaptor.forClass(IncomingAMQPEnvelope.class);
+        Mockito.verify(context).fireRead(argument.capture());
+        Mockito.verifyNoMoreInteractions(context);
+
+        assertNotNull(argument.getValue());
+        assertTrue(argument.getValue() instanceof EmptyEnvelope);
+    }
+
+    /*
+     * Test that two empty frames, as used for heartbeating, decode as expected when arriving back to back.
+     */
+    @Test
+    public void testDecodeMultipleEmptyFrames() throws Exception {
+        // http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#doc-idp124752
+        // Description: 2x '8byte sized' empty AMQP frames
+        byte[] emptyFrames = new byte[] { (byte) 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00,
+                                          (byte) 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00 };
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+
+        handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(emptyFrames));
+
+        ArgumentCaptor<IncomingAMQPEnvelope> argument = ArgumentCaptor.forClass(IncomingAMQPEnvelope.class);
+        Mockito.verify(context, Mockito.times(2)).fireRead(argument.capture());
+
+        List<IncomingAMQPEnvelope> frames = argument.getAllValues();
+        assertNotNull(frames);
+        assertEquals(2, frames.size());
+        assertTrue(frames.get(0) instanceof EmptyEnvelope);
+        assertTrue(frames.get(1) instanceof EmptyEnvelope);
+
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    /*
+     * Test that frames indicating they are under 8 bytes (the minimum size of the frame header) causes an error.
+     */
+    @Test
+    public void testInputOfFrameWithInvalidSizeBelowMinimumPossible() throws Exception {
+        // http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#doc-idp124752
+        // Description: '7byte sized' AMQP frame header
+        byte[] undersizedFrameHeader = new byte[] { (byte) 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x00 };
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+
+        try {
+            handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(undersizedFrameHeader));
+            fail("Should indicate protocol has been violated.");
+        } catch (ProtocolViolationException pve) {
+            // Expected
+            assertThat(pve.getMessage(), containsString("frame size 7 smaller than minimum"));
+        }
+
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    /*
+     * Test that frames indicating a DOFF under 8 bytes (the minimum size of the frame header) causes an error.
+     */
+    @Test
+    public void testInputOfFrameWithInvalidDoffBelowMinimumPossible() throws Exception {
+        // http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#doc-idp124752
+        // Description: '8byte sized' AMQP frame header with invalid doff of 1[*4 = 4bytes]
+        byte[] underMinDoffFrameHeader = new byte[] { (byte) 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00 };
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+
+        try {
+            handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(underMinDoffFrameHeader));
+            fail("Should indicate protocol has been violated.");
+        } catch (ProtocolViolationException pve) {
+            // Expected
+            assertThat(pve.getMessage(), containsString("data offset 4 smaller than minimum"));
+        }
+
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    /*
+     * Test that frames indicating a DOFF larger than the frame size cause expected error.
+     */
+    @Test
+    public void testInputOfFrameWithInvalidDoffAboveMaximumPossible() throws Exception {
+        // http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#doc-idp124752
+        // Description: '8byte sized' AMQP frame header with invalid doff of 3[*4 = 12bytes]
+        byte[] overFrameSizeDoffFrameHeader = new byte[] { (byte) 0x00, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, 0x00 };
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+
+        try {
+            handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(overFrameSizeDoffFrameHeader));
+            fail("Should indicate protocol has been violated.");
+        } catch (ProtocolViolationException pve) {
+            // Expected
+            assertThat(pve.getMessage(), containsString("data offset 12 larger than the frame size 8"));
+        }
+
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    /*
+     * Test that frame size above limit triggers error before attempting to decode the frame
+     */
+    @Test
+    public void testFrameSizeThatExceedsMaximumFrameSizeLimitTriggersError() throws Exception {
+        byte[] overFrameSizeLimitFrameHeader = new byte[] { (byte) 0xA0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 };
+
+        ProtonFrameDecodingHandler handler = createFrameDecoder();
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+
+        handler.handleRead(context, AMQPHeader.getAMQPHeader().getBuffer());
+
+        Mockito.verify(context).fireRead(Mockito.any(HeaderEnvelope.class));
+        Mockito.verifyNoMoreInteractions(context);
+
+        try {
+            handler.handleRead(context, ProtonByteBufferAllocator.DEFAULT.wrap(overFrameSizeLimitFrameHeader));
+            fail("Should indicate frame limit has been violated.");
+        } catch (ProtocolViolationException pve) {
+            // Expected 2684354560 frame size is to big
+            assertThat(pve.getMessage(), containsString("2684354560"));
+            assertThat(pve.getMessage(), containsString("larger than maximum frame size"));
+        }
+
+        Mockito.verifyNoMoreInteractions(context);
+    }
+
+    private ProtonFrameDecodingHandler createFrameDecoder() {
+        ProtonEngineConfiguration configuration = Mockito.mock(ProtonEngineConfiguration.class);
+        Mockito.when(configuration.getInboundMaxFrameSize()).thenReturn(Long.valueOf(65535));
+        ProtonEngine engine = Mockito.mock(ProtonEngine.class);
+        Mockito.when(engine.configuration()).thenReturn(configuration);
+        Mockito.when(engine.isWritable()).thenReturn(Boolean.TRUE);
+        EngineHandlerContext context = Mockito.mock(EngineHandlerContext.class);
+        Mockito.when(context.engine()).thenReturn(engine);
+
+        ProtonFrameDecodingHandler handler = new ProtonFrameDecodingHandler();
+        handler.handlerAdded(context);
+
+        return handler;
+    }
+
+    private Engine createEngine() {
+        ProtonEngine engine = new ProtonEngine();
+
+        engine.pipeline().addLast("read-sink", new FrameReadSinkTransportHandler());
+        engine.pipeline().addLast("test", testHandler);
+        engine.pipeline().addLast("frames", new ProtonFrameDecodingHandler());
+        engine.pipeline().addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        return engine;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameEncodingHandlerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameEncodingHandlerTest.java
new file mode 100644
index 0000000..fb53d33
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonFrameEncodingHandlerTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.codec.CodecFactory;
+import org.apache.qpid.protonj2.codec.Decoder;
+import org.apache.qpid.protonj2.codec.DecoderState;
+import org.apache.qpid.protonj2.engine.AMQPPerformativeEnvelopePool;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.types.transport.Transfer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class ProtonFrameEncodingHandlerTest {
+
+    private static final int FRAME_DOFF_SIZE = 2;
+    private static final byte AMQP_FRAME_TYPE = (byte) 0;
+
+    private AMQPPerformativeEnvelopePool<OutgoingAMQPEnvelope> framePool;
+
+    private ProtonEngineConfiguration configuration;
+    private ProtonEngine engine;
+    private EngineHandlerContext context;
+
+    private final Random random = new Random();
+    private final long randomSeed = System.currentTimeMillis();
+
+    @BeforeEach
+    void setUp() {
+        random.setSeed(randomSeed);
+
+        framePool = AMQPPerformativeEnvelopePool.outgoingEnvelopePool();
+
+        configuration = Mockito.mock(ProtonEngineConfiguration.class);
+        Mockito.when(configuration.getInboundMaxFrameSize()).thenReturn(Long.valueOf(65535));
+        Mockito.when(configuration.getOutboundMaxFrameSize()).thenReturn(Long.valueOf(65535));
+        Mockito.when(configuration.getBufferAllocator()).thenReturn(ProtonByteBufferAllocator.DEFAULT);
+
+        engine = Mockito.mock(ProtonEngine.class);
+        Mockito.when(engine.configuration()).thenReturn(configuration);
+        Mockito.when(engine.isWritable()).thenReturn(Boolean.TRUE);
+
+        context = Mockito.mock(ProtonEngineHandlerContext.class);
+        Mockito.when(context.engine()).thenReturn(engine);
+    }
+
+    @Test
+    void testEncodeBasicTransfer() {
+        ProtonFrameEncodingHandler handler = new ProtonFrameEncodingHandler();
+        handler.handlerAdded(context);
+
+        Transfer transfer = new Transfer();
+        transfer.setHandle(0);
+        transfer.setDeliveryId(0);
+        transfer.setDeliveryTag(new byte[] {0});
+
+        OutgoingAMQPEnvelope frame = framePool.take(transfer, 32, null);
+
+        handler.handleWrite(context, frame);
+
+        ArgumentCaptor<ProtonBuffer> argument = ArgumentCaptor.forClass(ProtonBuffer.class);
+        Mockito.verify(context).fireWrite(argument.capture(), Mockito.any(Runnable.class));
+
+        ProtonBuffer output = argument.getValue();
+
+        assertNotNull(output);
+        assertTrue(output.getWriteIndex() > 0);
+
+        final int bufferSize = output.getReadableBytes();
+
+        assertEquals(bufferSize, output.readInt());
+        assertEquals(FRAME_DOFF_SIZE, output.readByte());
+        assertEquals(AMQP_FRAME_TYPE, output.readByte());
+        assertEquals(32, output.readShort());
+
+        final Transfer decodedTransfer = decode(output);
+        assertEquals(transfer.getHandle(), decodedTransfer.getHandle());
+        assertEquals(transfer.getDeliveryId(), decodedTransfer.getDeliveryId());
+        assertEquals(transfer.getDeliveryTag(), decodedTransfer.getDeliveryTag());
+        assertEquals(transfer.getMore(), decodedTransfer.getMore());
+    }
+
+    @Test
+    void testEncodeBasicTransferWthPayloadThatFitsIntoFrame() {
+        ProtonFrameEncodingHandler handler = new ProtonFrameEncodingHandler();
+        handler.handlerAdded(context);
+
+        Transfer transfer = new Transfer();
+        transfer.setHandle(0);
+        transfer.setDeliveryId(0);
+        transfer.setDeliveryTag(new byte[] {0});
+
+        final byte[] payload = new byte[64];
+
+        random.nextBytes(payload);
+
+        OutgoingAMQPEnvelope frame = framePool.take(transfer, 32, ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        handler.handleWrite(context, frame);
+
+        ArgumentCaptor<ProtonBuffer> argument = ArgumentCaptor.forClass(ProtonBuffer.class);
+        Mockito.verify(context).fireWrite(argument.capture(), Mockito.any(Runnable.class));
+
+        ProtonBuffer output = argument.getValue();
+
+        assertNotNull(output);
+        assertTrue(output.getWriteIndex() > 0);
+
+        final int bufferSize = output.getReadableBytes();
+
+        assertEquals(bufferSize, output.readInt());
+        assertEquals(FRAME_DOFF_SIZE, output.readByte());
+        assertEquals(AMQP_FRAME_TYPE, output.readByte());
+        assertEquals(32, output.readShort());
+
+        final Transfer decodedTransfer = decode(output);
+        assertEquals(transfer.getHandle(), decodedTransfer.getHandle());
+        assertEquals(transfer.getDeliveryId(), decodedTransfer.getDeliveryId());
+        assertEquals(transfer.getDeliveryTag(), decodedTransfer.getDeliveryTag());
+        assertEquals(transfer.getMore(), decodedTransfer.getMore());
+    }
+
+    @Test
+    void testEncodeBasicTransferWthPayloadThatDoesNotFitIntoFrame() {
+        ProtonFrameEncodingHandler handler = new ProtonFrameEncodingHandler();
+        handler.handlerAdded(context);
+
+        Transfer transfer = new Transfer();
+        transfer.setHandle(0);
+        transfer.setDeliveryId(0);
+        transfer.setDeliveryTag(new byte[] {0});
+
+        final byte[] payload = new byte[(int) (configuration.getOutboundMaxFrameSize() * 2)];
+        final AtomicBoolean toLargeHandlerCalled = new AtomicBoolean();
+
+        random.nextBytes(payload);
+
+        OutgoingAMQPEnvelope frame = framePool.take(transfer, 32, ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        frame.setPayloadToLargeHandler((performative) -> {
+            transfer.setMore(true);
+            toLargeHandlerCalled.set(true);
+        });
+
+        handler.handleWrite(context, frame);
+
+        ArgumentCaptor<ProtonBuffer> argument = ArgumentCaptor.forClass(ProtonBuffer.class);
+        Mockito.verify(context).fireWrite(argument.capture(), Mockito.any(Runnable.class));
+
+        ProtonBuffer output = argument.getValue();
+
+        assertTrue(toLargeHandlerCalled.get());
+        assertNotNull(output);
+        assertEquals(output.getReadableBytes(), configuration.getOutboundMaxFrameSize());
+
+        final int bufferSize = output.getReadableBytes();
+
+        assertEquals(configuration.getOutboundMaxFrameSize(), output.maxCapacity());
+        assertEquals(bufferSize, output.readInt());
+        assertEquals(FRAME_DOFF_SIZE, output.readByte());
+        assertEquals(AMQP_FRAME_TYPE, output.readByte());
+        assertEquals(32, output.readShort());
+
+        final Transfer decodedTransfer = decode(output);
+        assertEquals(transfer.getHandle(), decodedTransfer.getHandle());
+        assertEquals(transfer.getDeliveryId(), decodedTransfer.getDeliveryId());
+        assertEquals(transfer.getDeliveryTag(), decodedTransfer.getDeliveryTag());
+        assertEquals(transfer.getMore(), decodedTransfer.getMore());
+    }
+
+    @Test
+    void testOutgoingFrameIsReleasedAfterWriteFinishes() {
+        ProtonFrameEncodingHandler handler = new ProtonFrameEncodingHandler();
+        handler.handlerAdded(context);
+
+        Transfer transfer = new Transfer();
+        transfer.setHandle(0);
+        transfer.setDeliveryId(0);
+        transfer.setDeliveryTag(new byte[] {0});
+
+        final byte[] payload = new byte[64];
+
+        random.nextBytes(payload);
+
+        OutgoingAMQPEnvelope frame = Mockito.spy(framePool.take(transfer, 32, ProtonByteBufferAllocator.DEFAULT.wrap(payload)));
+
+        handler.handleWrite(context, frame);
+
+        ArgumentCaptor<ProtonBuffer> argument = ArgumentCaptor.forClass(ProtonBuffer.class);
+        Mockito.verify(context).fireWrite(argument.capture(), Mockito.any(Runnable.class));
+        Mockito.verify(frame, Mockito.never()).release();
+
+        ProtonBuffer output = argument.getValue();
+
+        assertNotNull(output);
+        assertTrue(output.getWriteIndex() > 0);
+
+        final int bufferSize = output.getReadableBytes();
+
+        assertEquals(bufferSize, output.readInt());
+        assertEquals(FRAME_DOFF_SIZE, output.readByte());
+        assertEquals(AMQP_FRAME_TYPE, output.readByte());
+        assertEquals(32, output.readShort());
+
+        final Transfer decodedTransfer = decode(output);
+        assertEquals(transfer.getHandle(), decodedTransfer.getHandle());
+        assertEquals(transfer.getDeliveryId(), decodedTransfer.getDeliveryId());
+        assertEquals(transfer.getDeliveryTag(), decodedTransfer.getDeliveryTag());
+        assertEquals(transfer.getMore(), decodedTransfer.getMore());
+    }
+
+    private Transfer decode(ProtonBuffer encoded) {
+        Decoder decoder = CodecFactory.getDecoder();
+        DecoderState decoderState = decoder.newDecoderState();
+
+        Object decoded = decoder.readObject(encoded, decoderState);
+        assertTrue(decoded instanceof Transfer);
+
+        return (Transfer) decoded;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonIncomingDeliveryTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonIncomingDeliveryTest.java
new file mode 100644
index 0000000..040db3d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonIncomingDeliveryTest.java
@@ -0,0 +1,263 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.times;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class ProtonIncomingDeliveryTest extends ProtonEngineTestSupport {
+
+    public static final int DEFAULT_MESSAGE_FORMAT = 0;
+
+    @Test
+    public void testToStringOnEmptyDeliveryDoesNotNPE() throws Exception {
+        ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            Mockito.mock(ProtonReceiver.class), 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+        assertNotNull(delivery.toString());
+    }
+
+    @Test
+    public void testDefaultMessageFormat() throws Exception {
+        ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            Mockito.mock(ProtonReceiver.class), 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+        assertEquals(0L, DEFAULT_MESSAGE_FORMAT, "Unexpected value");
+        assertEquals(DEFAULT_MESSAGE_FORMAT, delivery.getMessageFormat(), "Unexpected message format");
+    }
+
+    @Test
+    public void testAvailable() throws Exception {
+        byte[] data = "test-data".getBytes(StandardCharsets.UTF_8);
+
+        ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            Mockito.mock(ProtonReceiver.class), 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+        delivery.appendTransferPayload(ProtonByteBufferAllocator.DEFAULT.wrap(data));
+
+        // Check the full data is available
+        assertNotNull(delivery, "expected the delivery to be present");
+        assertEquals(data.length, delivery.available(), "unexpectd available count");
+
+        // Extract some of the data as the receiver link will, check available gets reduced accordingly.
+        int partLength = 2;
+        int remainderLength = data.length - partLength;
+        assertTrue(partLength < data.length);
+
+        byte[] myRecievedData1 = new byte[partLength];
+
+        delivery.readBytes(myRecievedData1, 0, myRecievedData1.length);
+        assertEquals(remainderLength, delivery.available(), "Unexpected data length available");
+
+        // Extract remainder of the data as the receiver link will, check available hits 0.
+        byte[] myRecievedData2 = new byte[remainderLength];
+
+        delivery.readBytes(myRecievedData2, 0, remainderLength);
+        assertEquals(0, delivery.available(), "Expected no data to remain available");
+    }
+
+    @Test
+    public void testAvailableWhenEmpty() throws Exception {
+        ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            Mockito.mock(ProtonReceiver.class), 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+        assertEquals(0, delivery.available());
+    }
+
+    @Test
+    public void testAppendArraysToBuffer() throws Exception {
+        ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            Mockito.mock(ProtonReceiver.class), 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        byte[] data1 = new byte[] { 0, 1, 2, 3, 4, 5 };
+        byte[] data2 = new byte[] { 6, 7, 8, 9, 10, 11 };
+
+        assertTrue(delivery.isFirstTransfer());
+        assertEquals(0, delivery.getTransferCount());
+        delivery.appendTransferPayload(ProtonByteBufferAllocator.DEFAULT.wrap(data1));
+        assertTrue(delivery.isFirstTransfer());
+        assertEquals(1, delivery.getTransferCount());
+        delivery.appendTransferPayload(ProtonByteBufferAllocator.DEFAULT.wrap(data2));
+        assertFalse(delivery.isFirstTransfer());
+        assertEquals(2, delivery.getTransferCount());
+
+        assertEquals(data1.length + data2.length, delivery.available());
+        assertEquals(data1.length + data2.length, delivery.readAll().getReadableBytes());
+        assertNull(delivery.readAll());
+    }
+
+    @Test
+    public void testClaimAvailableBytesIndicatesAllBytesRead() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver).deliveryRead(delivery, 1024);
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    @Test
+    public void testReadAllAfterAllClaimedDoesNotClaimMore() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver).deliveryRead(delivery, 1024);
+
+        assertNotNull(delivery.readAll());
+
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    @Test
+    public void testReadAllAfterAllClaimedSignalsBytesReadIfMoreDataArrived() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver).deliveryRead(delivery, 1024);
+
+        delivery.appendTransferPayload(createProtonBuffer(512));
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+        delivery.appendTransferPayload(createProtonBuffer(256));
+        delivery.appendTransferPayload(createProtonBuffer(256));
+
+        assertNotNull(delivery.readAll());
+
+        Mockito.verify(receiver).deliveryRead(delivery, 2048);
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    @Test
+    public void testClaimAvailableBytesDoesNothingOnSecondCall() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver).deliveryRead(delivery, 1024);
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    @Test
+    public void testClaimAvailableBytesIndicatesAllBytesReadAfterNewDelivery() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        delivery.appendTransferPayload(createProtonBuffer(512));
+
+        assertEquals(1024 + 512, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver, times(1)).deliveryRead(delivery, 1024);
+        Mockito.verify(receiver, times(1)).deliveryRead(delivery, 512);
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    @Test
+    public void testClaimAvailableBytesThenReadSomeAndExpectNoMoreClaimed() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        byte[] target = new byte[512];
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver, times(1)).deliveryRead(delivery, 1024);
+
+        delivery.readBytes(target, 0, target.length);
+        delivery.readBytes(target, 0, target.length);
+
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    @Test
+    public void testClaimThenReadSomeGetMoreAndThenClaimAgain() throws Exception {
+        final ProtonReceiver receiver = Mockito.mock(ProtonReceiver.class);
+        final ProtonIncomingDelivery delivery = new ProtonIncomingDelivery(
+            receiver, 1, new DeliveryTag.ProtonDeliveryTag(new byte[] {0}));
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        byte[] target = new byte[2048];
+
+        assertEquals(1024, delivery.available());
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verify(receiver, times(1)).deliveryRead(delivery, 1024);
+
+        delivery.appendTransferPayload(createProtonBuffer(1024));
+
+        delivery.readBytes(target, 0, target.length);
+
+        Mockito.verify(receiver, times(2)).deliveryRead(delivery, 1024);
+        Mockito.verifyNoMoreInteractions(receiver);
+
+        assertSame(delivery, delivery.claimAvailableBytes());
+
+        Mockito.verifyNoMoreInteractions(receiver);
+    }
+
+    private ProtonBuffer createProtonBuffer(int available) {
+        byte[] array = new byte[available];
+        Arrays.fill(array, (byte) 65);
+        return ProtonByteBufferAllocator.DEFAULT.wrap(array);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonOutgoingDeliveryTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonOutgoingDeliveryTest.java
new file mode 100644
index 0000000..8a1ca46
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonOutgoingDeliveryTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class ProtonOutgoingDeliveryTest extends ProtonEngineTestSupport {
+
+    public static final int DEFAULT_MESSAGE_FORMAT = 0;
+
+    @Test
+    public void testToStringOnEmptyDeliveryDoesNotNPE() throws Exception {
+        ProtonOutgoingDelivery delivery = new ProtonOutgoingDelivery(Mockito.mock(ProtonSender.class));
+        assertNotNull(delivery.toString());
+    }
+
+    @Test
+    public void testDefaultMessageFormat() throws Exception {
+        ProtonOutgoingDelivery delivery = new ProtonOutgoingDelivery(Mockito.mock(ProtonSender.class));
+
+        assertEquals(0L, DEFAULT_MESSAGE_FORMAT, "Unexpected value");
+        assertEquals(DEFAULT_MESSAGE_FORMAT, delivery.getMessageFormat(), "Unexpected message format");
+    }
+
+    @Test
+    public void testSetGetMessageFormat() throws Exception {
+        ProtonOutgoingDelivery delivery = new ProtonOutgoingDelivery(Mockito.mock(ProtonSender.class));
+
+        // lowest value and default
+        int newFormat = 0;
+        delivery.setMessageFormat(newFormat);
+        assertEquals(newFormat, delivery.getMessageFormat(), "Unexpected message format");
+
+        newFormat = 123456;
+        delivery.setMessageFormat(newFormat);
+        assertEquals(newFormat, delivery.getMessageFormat(), "Unexpected message format");
+
+        // Highest value
+        newFormat = (1 << 32) - 1;
+        delivery.setMessageFormat(newFormat);
+        assertEquals(newFormat, delivery.getMessageFormat(), "Unexpected message format");
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonPooledTagGeneratorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonPooledTagGeneratorTest.java
new file mode 100644
index 0000000..b152bf7
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonPooledTagGeneratorTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.ArrayList;
+
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.junit.jupiter.api.Test;
+
+public class ProtonPooledTagGeneratorTest {
+
+    @Test
+    public void testCreateTagGenerator() {
+        DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+        assertTrue(generator instanceof ProtonPooledTagGenerator);
+    }
+
+    @Test
+    public void testCreateTagGeneratorChecksPoolSze() {
+        try {
+            new ProtonPooledTagGenerator(0);
+            fail("Should not allow non-pooling pool");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            new ProtonPooledTagGenerator(-1);
+            fail("Should not allow negative sized pool");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testCreateTag() {
+        ProtonPooledTagGenerator generator = new ProtonPooledTagGenerator();
+        assertNotNull(generator.nextTag());
+    }
+
+    @Test
+    public void testCreateTagsFromPoolAndReturn() {
+        ProtonPooledTagGenerator generator = new ProtonPooledTagGenerator();
+
+        final ArrayList<DeliveryTag> tags = new ArrayList<>(ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS);
+
+        for (int i = 0; i < ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS; ++i) {
+            tags.add(generator.nextTag());
+        }
+
+        tags.forEach(tag -> tag.release());
+
+        for (int i = 0; i < ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS; ++i) {
+            assertSame(tags.get(i), generator.nextTag());
+        }
+
+        DeliveryTag nonCached = generator.nextTag();
+        assertFalse(tags.contains(nonCached));
+        nonCached.release();
+        assertFalse(tags.contains(nonCached));
+    }
+
+    @Test
+    public void testConsumeAllPooledTagsAndThenReleaseAfterCreatingNonPooled() {
+        ProtonPooledTagGenerator generator = new ProtonPooledTagGenerator();
+
+        DeliveryTag pooledTag = generator.nextTag();
+        DeliveryTag nonCached = generator.nextTag();
+
+        assertNotSame(pooledTag, nonCached);
+
+        pooledTag.release();
+        nonCached.release();
+
+        DeliveryTag shouldBeCached = generator.nextTag();
+
+        assertSame(pooledTag, shouldBeCached);
+    }
+
+    @Test
+    public void testPooledTagReleaseIsIdempotent() {
+        ProtonPooledTagGenerator generator = new ProtonPooledTagGenerator();
+
+        DeliveryTag pooledTag = generator.nextTag();
+
+        pooledTag.release();
+        pooledTag.release();
+        pooledTag.release();
+
+        assertSame(pooledTag, generator.nextTag());
+        assertNotSame(pooledTag, generator.nextTag());
+        assertNotSame(pooledTag, generator.nextTag());
+    }
+
+    @Test
+    public void testCreateTagsThatWrapAroundLimit() {
+        ProtonPooledTagGenerator generator = new ProtonPooledTagGenerator();
+
+        final ArrayList<DeliveryTag> tags = new ArrayList<>(ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS);
+
+        for (int i = 0; i < ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS; ++i) {
+            tags.add(generator.nextTag());
+        }
+
+        // Test that on wrap the tags start beyond the pooled values.
+        generator.setNextTagId(0xFFFFFFFFFFFFFFFFl);
+
+        DeliveryTag maxUnsignedLong = generator.nextTag();
+        DeliveryTag nextTagAfterWrap = generator.nextTag();
+
+        assertEquals(Long.BYTES, maxUnsignedLong.tagBytes().length);
+        assertEquals(Short.BYTES, nextTagAfterWrap.tagBytes().length);
+
+        final short tagValue = getShort(nextTagAfterWrap.tagBytes());
+
+        assertEquals(ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS, tagValue);
+
+        tags.get(0).release();
+
+        DeliveryTag tagAfterRelease = generator.nextTag();
+
+        assertSame(tags.get(0), tagAfterRelease);
+    }
+
+    @Test
+    public void testTakeAllTagsReturnThemAndTakeThemAgainDefaultSize() {
+        doTestTakeAllTagsReturnThemAndTakeThemAgain(-1);
+    }
+
+    @Test
+    public void testTakeAllTagsReturnThemAndTakeThemAgain() {
+        doTestTakeAllTagsReturnThemAndTakeThemAgain(64);
+    }
+
+    private void doTestTakeAllTagsReturnThemAndTakeThemAgain(int poolSize) {
+        final ProtonPooledTagGenerator generator;
+        if (poolSize == -1) {
+            generator = new ProtonPooledTagGenerator();
+            poolSize = ProtonPooledTagGenerator.DEFAULT_MAX_NUM_POOLED_TAGS;
+        } else {
+            generator = new ProtonPooledTagGenerator(poolSize);
+        }
+
+        final ArrayList<DeliveryTag> tags1 = new ArrayList<>(poolSize);
+        final ArrayList<DeliveryTag> tags2 = new ArrayList<>(poolSize);
+
+        for (int i = 0; i < poolSize; ++i) {
+            tags1.add(generator.nextTag());
+        }
+
+        for (int i = 0; i < poolSize; ++i) {
+            tags1.get(i).release();
+        }
+
+        for (int i = 0; i < poolSize; ++i) {
+            tags2.add(generator.nextTag());
+        }
+
+        for (int i = 0; i < poolSize; ++i) {
+            assertSame(tags1.get(i), tags2.get(i));
+        }
+    }
+
+    private short getShort(byte[] tagBytes) {
+        return (short) ((tagBytes[0] & 0xFF) << 8 | (tagBytes[1] & 0xFF) << 0);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonReceiverTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonReceiverTest.java
new file mode 100644
index 0000000..7b32e69
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonReceiverTest.java
@@ -0,0 +1,4595 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.LinkState;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineShutdownException;
+import org.apache.qpid.protonj2.engine.util.SimplePojo;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Data;
+import org.apache.qpid.protonj2.types.messaging.MessageAnnotations;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Properties;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Section;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test the {@link ProtonReceiver}
+ */
+@Timeout(20)
+public class ProtonReceiverTest extends ProtonEngineTestSupport {
+
+    public static final Symbol[] SUPPORTED_OUTCOMES = new Symbol[] { Accepted.DESCRIPTOR_SYMBOL,
+                                                                     Rejected.DESCRIPTOR_SYMBOL,
+                                                                     Released.DESCRIPTOR_SYMBOL,
+                                                                     Modified.DESCRIPTOR_SYMBOL };
+
+    @Test
+    public void testLocalLinkStateCannotBeChangedAfterOpen() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+
+        receiver.setProperties(new HashMap<>());
+
+        receiver.open();
+
+        try {
+            receiver.setProperties(new HashMap<>());
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            receiver.setDesiredCapabilities(new Symbol[] { AmqpError.DECODE_ERROR });
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            receiver.setOfferedCapabilities(new Symbol[] { AmqpError.DECODE_ERROR });
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            receiver.setSenderSettleMode(SenderSettleMode.MIXED);
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            receiver.setSource(new Source());
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            receiver.setTarget(new Target());
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            receiver.setMaxMessageSize(UnsignedLong.ZERO);
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        receiver.detach();
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverEmitsOpenAndCloseEvents() throws Exception {
+        doTestReceiverEmitsEvents(false);
+    }
+
+    @Test
+    public void testReceiverEmitsOpenAndDetachEvents() throws Exception {
+        doTestReceiverEmitsEvents(true);
+    }
+
+    private void doTestReceiverEmitsEvents(boolean detach) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean receiverLocalOpen = new AtomicBoolean();
+        final AtomicBoolean receiverLocalClose = new AtomicBoolean();
+        final AtomicBoolean receiverLocalDetach = new AtomicBoolean();
+        final AtomicBoolean receiverRemoteOpen = new AtomicBoolean();
+        final AtomicBoolean receiverRemoteClose = new AtomicBoolean();
+        final AtomicBoolean receiverRemoteDetach = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.localOpenHandler(result -> receiverLocalOpen.set(true))
+                .localCloseHandler(result -> receiverLocalClose.set(true))
+                .localDetachHandler(result -> receiverLocalDetach.set(true))
+                .openHandler(result -> receiverRemoteOpen.set(true))
+                .detachHandler(result -> receiverRemoteDetach.set(true))
+                .closeHandler(result -> receiverRemoteClose.set(true));
+
+        receiver.open();
+
+        if (detach) {
+            receiver.detach();
+        } else {
+            receiver.close();
+        }
+
+        assertTrue(receiverLocalOpen.get(), "Receiver should have reported local open");
+        assertTrue(receiverRemoteOpen.get(), "Receiver should have reported remote open");
+
+        if (detach) {
+            assertFalse(receiverLocalClose.get(), "Receiver should not have reported local close");
+            assertTrue(receiverLocalDetach.get(), "Receiver should have reported local detach");
+            assertFalse(receiverRemoteClose.get(), "Receiver should not have reported remote close");
+            assertTrue(receiverRemoteDetach.get(), "Receiver should have reported remote close");
+        } else {
+            assertTrue(receiverLocalClose.get(), "Receiver should have reported local close");
+            assertFalse(receiverLocalDetach.get(), "Receiver should not have reported local detach");
+            assertTrue(receiverRemoteClose.get(), "Receiver should have reported remote close");
+            assertFalse(receiverRemoteDetach.get(), "Receiver should not have reported remote close");
+        }
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverRoutesDetachEventToCloseHandlerIfNonSset() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean receiverLocalOpen = new AtomicBoolean();
+        final AtomicBoolean receiverLocalClose = new AtomicBoolean();
+        final AtomicBoolean receiverRemoteOpen = new AtomicBoolean();
+        final AtomicBoolean receiverRemoteClose = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.localOpenHandler(result -> receiverLocalOpen.set(true))
+                .localCloseHandler(result -> receiverLocalClose.set(true))
+                .openHandler(result -> receiverRemoteOpen.set(true))
+                .closeHandler(result -> receiverRemoteClose.set(true));
+
+        receiver.open();
+        receiver.detach();
+
+        assertTrue(receiverLocalOpen.get(), "Receiver should have reported local open");
+        assertTrue(receiverRemoteOpen.get(), "Receiver should have reported remote open");
+        assertTrue(receiverLocalClose.get(), "Receiver should have reported local detach");
+        assertTrue(receiverRemoteClose.get(), "Receiver should have reported remote detach");
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineShutdownEventNeitherEndClosed() throws Exception {
+        doTestEngineShutdownEvent(false, false);
+    }
+
+    @Test
+    public void testEngineShutdownEventLocallyClosed() throws Exception {
+        doTestEngineShutdownEvent(true, false);
+    }
+
+    @Test
+    public void testEngineShutdownEventRemotelyClosed() throws Exception {
+        doTestEngineShutdownEvent(false, true);
+    }
+
+    @Test
+    public void testEngineShutdownEventBothEndsClosed() throws Exception {
+        doTestEngineShutdownEvent(true, true);
+    }
+
+    private void doTestEngineShutdownEvent(boolean locallyClosed, boolean remotelyClosed) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean engineShutdown = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+        receiver.engineShutdownHandler(result -> engineShutdown.set(true));
+
+        if (locallyClosed) {
+            if (remotelyClosed) {
+                peer.expectDetach().respond();
+            } else {
+                peer.expectDetach();
+            }
+
+            receiver.close();
+        }
+
+        if (remotelyClosed && !locallyClosed) {
+            peer.remoteDetach().now();
+        }
+
+        engine.shutdown();
+
+        if (locallyClosed && remotelyClosed) {
+            assertFalse(engineShutdown.get(), "Should not have reported engine shutdown");
+        } else {
+            assertTrue(engineShutdown.get(), "Should have reported engine shutdown");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverOpenWithNoSenderOrReceiverSettleModes() throws Exception {
+        doTestOpenReceiverWithConfiguredSenderAndReceiverSettlementModes(null, null);
+    }
+
+    @Test
+    public void testReceiverOpenWithSettledAndFirst() throws Exception {
+        doTestOpenReceiverWithConfiguredSenderAndReceiverSettlementModes(SenderSettleMode.SETTLED, ReceiverSettleMode.FIRST);
+    }
+
+    @Test
+    public void testReceiverOpenWithUnsettledAndSecond() throws Exception {
+        doTestOpenReceiverWithConfiguredSenderAndReceiverSettlementModes(SenderSettleMode.UNSETTLED, ReceiverSettleMode.SECOND);
+    }
+
+    private void doTestOpenReceiverWithConfiguredSenderAndReceiverSettlementModes(SenderSettleMode senderMode, ReceiverSettleMode receiverMode) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withSndSettleMode(senderMode == null ? null : senderMode.byteValue())
+                           .withRcvSettleMode(receiverMode == null ? null : receiverMode.byteValue())
+                           .respond()
+                           .withSndSettleMode(senderMode == null ? null : senderMode.byteValue())
+                           .withRcvSettleMode(receiverMode == null ? null : receiverMode.byteValue());
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.setSenderSettleMode(senderMode);
+        receiver.setReceiverSettleMode(receiverMode);
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().respond();
+
+        if (senderMode != null) {
+            assertEquals(senderMode, receiver.getSenderSettleMode());
+        } else {
+            assertEquals(SenderSettleMode.MIXED, receiver.getSenderSettleMode());
+        }
+        if (receiverMode != null) {
+            assertEquals(receiverMode, receiver.getReceiverSettleMode());
+        } else {
+            assertEquals(ReceiverSettleMode.FIRST, receiver.getReceiverSettleMode());
+        }
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCreateReceiverAndInspectRemoteEndpoint() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withTarget(notNullValue())
+                           .withTarget(notNullValue())
+                           .respond();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        final Modified defaultOutcome = new Modified().setDeliveryFailed(true);
+        final String sourceAddress = UUID.randomUUID().toString() + ":1";
+
+        Source source = new Source();
+        source.setAddress(sourceAddress);
+        source.setOutcomes(SUPPORTED_OUTCOMES);
+        source.setDefaultOutcome(defaultOutcome);
+
+        Receiver receiver = session.receiver("test");
+        receiver.setSource(source);
+        receiver.setTarget(new Target());
+        receiver.open();
+
+        assertTrue(receiver.getRemoteState().equals(LinkState.ACTIVE));
+        assertNotNull(receiver.getRemoteSource());
+        assertNotNull(receiver.getRemoteTarget());
+        assertArrayEquals(SUPPORTED_OUTCOMES, receiver.getRemoteSource().getOutcomes());
+        assertTrue(receiver.getRemoteSource().getDefaultOutcome() instanceof Modified);
+        assertEquals(sourceAddress, receiver.getRemoteSource().getAddress());
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCreateReceiverAndClose() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLink(true);
+    }
+
+    @Test
+    public void testCreateReceiverAndDetach() throws Exception {
+        doTestCreateReceiverAndCloseOrDetachLink(false);
+    }
+
+    private void doTestCreateReceiverAndCloseOrDetachLink(boolean close) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectDetach().withClosed(close).respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        assertTrue(receiver.isReceiver());
+        assertFalse(receiver.isSender());
+
+        if (close) {
+            receiver.close();
+        } else {
+            receiver.detach();
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverOpenAndCloseAreIdempotent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        // Should not emit another attach frame
+        receiver.open();
+
+        receiver.close();
+
+        // Should not emit another detach frame
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineEmitsAttachAfterLocalReceiverOpened() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenBeginAttachBeforeRemoteResponds() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.expectBegin();
+        peer.expectAttach();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverFireOpenedEventAfterRemoteAttachArrives() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.openHandler(result -> {
+            receiverRemotelyOpened.set(true);
+        });
+        receiver.open();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverFireClosedEventAfterRemoteDetachArrives() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean receiverRemotelyClosed = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.openHandler(result -> {
+            receiverRemotelyOpened.set(true);
+        });
+        receiver.closeHandler(result -> {
+            receiverRemotelyClosed.set(true);
+        });
+        receiver.open();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+
+        receiver.close();
+
+        assertTrue(receiverRemotelyClosed.get(), "Receiver remote closed event did not fire");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testRemotelyCloseReceiverAndOpenNewReceiverImmediatelyAfterWithNewLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(true, false);
+    }
+
+    @Test
+    public void testRemotelyDetachReceiverAndOpenNewReceiverImmediatelyAfterWithNewLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(false, false);
+    }
+
+    @Test
+    public void testRemotelyCloseReceiverAndOpenNewReceiverImmediatelyAfterWithSameLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(true, true);
+    }
+
+    @Test
+    public void testRemotelyDetachReceiverAndOpenNewReceiverImmediatelyAfterWithSameLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(false, true);
+    }
+
+    private void doTestRemotelyTerminateLinkAndThenCreateNewLink(boolean close, boolean sameLinkName) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        String firstLinkName = "test-link-1";
+        String secondLinkName = sameLinkName ? firstLinkName : "test-link-2";
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withRole(Role.RECEIVER.getValue()).respond();
+        peer.remoteDetach().withClosed(close).queue();
+
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean receiverRemotelyClosed = new AtomicBoolean();
+        final AtomicBoolean receiverRemotelyDetached = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver(firstLinkName);
+        receiver.openHandler(result -> receiverRemotelyOpened.set(true));
+        receiver.closeHandler(result -> receiverRemotelyClosed.set(true));
+        receiver.detachHandler(result -> receiverRemotelyDetached.set(true));
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+
+        if (close) {
+            assertTrue(receiverRemotelyClosed.get(), "Receiver remote closed event did not fire");
+            assertFalse(receiverRemotelyDetached.get(), "Receiver remote detached event fired");
+        } else {
+            assertFalse(receiverRemotelyClosed.get(), "Receiver remote closed event fired");
+            assertTrue(receiverRemotelyDetached.get(), "Receiver remote closed event did not fire");
+        }
+
+        peer.expectDetach().withClosed(close);
+        if (close) {
+            receiver.close();
+        } else {
+            receiver.detach();
+        }
+
+        peer.waitForScriptToComplete();
+        peer.expectAttach().withHandle(0).withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectDetach().withClosed(close).respond();
+
+        // Reset trackers
+        receiverRemotelyOpened.set(false);
+        receiverRemotelyClosed.set(false);
+        receiverRemotelyDetached.set(false);
+
+        receiver = session.receiver(secondLinkName);
+        receiver.openHandler(result -> receiverRemotelyOpened.set(true));
+        receiver.closeHandler(result -> receiverRemotelyClosed.set(true));
+        receiver.detachHandler(result -> receiverRemotelyDetached.set(true));
+        receiver.open();
+
+        if (close) {
+            receiver.close();
+        } else {
+            receiver.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+
+        if (close) {
+            assertTrue(receiverRemotelyClosed.get(), "Receiver remote closed event did not fire");
+            assertFalse(receiverRemotelyDetached.get(), "Receiver remote detached event fired");
+        } else {
+            assertFalse(receiverRemotelyClosed.get(), "Receiver remote closed event fired");
+            assertTrue(receiverRemotelyDetached.get(), "Receiver remote closed event did not fire");
+        }
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverFireOpenedEventAfterRemoteAttachArrivesWithNullTarget() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond().withNullSource();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.setSource(new Source());
+        receiver.setTarget(new Target());
+        receiver.openHandler(result -> {
+            receiverRemotelyOpened.set(true);
+        });
+        receiver.open();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+        assertNull(receiver.getRemoteSource());
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenAndCloseMultipleReceivers() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).respond();
+        peer.expectAttach().withHandle(1).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver1 = session.receiver("receiver-1");
+        receiver1.open();
+        Receiver receiver2 = session.receiver("receiver-2");
+        receiver2.open();
+
+        // Close in reverse order
+        receiver2.close();
+        receiver1.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionSignalsRemoteReceiverOpen() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("sender")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withInitialDeliveryCount(0).queue();
+        peer.expectAttach();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+        final AtomicReference<Receiver> receiver = new AtomicReference<>();
+
+        Connection connection = engine.start();
+
+        connection.receiverOpenHandler(result -> {
+            receiverRemotelyOpened.set(true);
+            receiver.set(result);
+        });
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+
+        receiver.get().open();
+        receiver.get().close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotOpenReceiverAfterSessionClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+
+        session.close();
+
+        try {
+            receiver.open();
+            fail("Should not be able to open a link from a closed session.");
+        } catch (IllegalStateException ise) {}
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotOpenReceiverAfterSessionRemotelyClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteEnd().queue();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        Receiver receiver = session.receiver("test");
+        session.open();
+
+        try {
+            receiver.open();
+            fail("Should not be able to open a link from a remotely closed session.");
+        } catch (IllegalStateException ise) {}
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenReceiverBeforeOpenConnection() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Create the connection but don't open, then open a session and a receiver and
+        // the session begin and receiver attach shouldn't go out until the connection
+        // is opened locally.
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("receiver");
+        receiver.open();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("receiver").withRole(Role.RECEIVER.getValue()).respond();
+
+        // Now open the connection, expect the Open, Begin, and Attach frames
+        connection.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenReceiverBeforeOpenSession() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        // Create the connection and open it, then create a session and a receiver
+        // and observe that the receiver doesn't send its attach until the session
+        // is opened.
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        Receiver receiver = session.receiver("receiver");
+        receiver.open();
+
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("receiver").withRole(Role.RECEIVER.getValue()).respond();
+
+        // Now open the session, expect the Begin, and Attach frames
+        session.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDetachAfterEndSent() {
+        doTestReceiverCloseOrDetachAfterEndSent(false);
+    }
+
+    @Test
+    public void testReceiverCloseAfterEndSent() {
+        doTestReceiverCloseOrDetachAfterEndSent(true);
+    }
+
+    public void doTestReceiverCloseOrDetachAfterEndSent(boolean close) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("receiver").withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectEnd().respond();
+
+        // Create the connection and open it, then create a session and a receiver
+        // and observe that the receiver doesn't send its detach if the session has
+        // already been closed.
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("receiver");
+        receiver.open();
+
+        // Cause an End frame to be sent
+        session.close();
+
+        // The sender should not emit an end as the session was closed which implicitly
+        // detached the link.
+        if (close) {
+            receiver.close();
+        } else {
+            receiver.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDetachAfterCloseSent() {
+        doTestReceiverClosedOrDetachedAfterCloseSent(false);
+    }
+
+    @Test
+    public void testReceiverCloseAfterCloseSent() {
+        doTestReceiverClosedOrDetachedAfterCloseSent(true);
+    }
+
+    public void doTestReceiverClosedOrDetachedAfterCloseSent(boolean close) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("receiver").withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectClose().respond();
+
+        // Create the connection and open it, then create a session and a receiver
+        // and observe that the receiver doesn't send its detach if the connection has
+        // already been closed.
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("receiver");
+        receiver.open();
+
+        // Cause an Close frame to be sent
+        connection.close();
+
+        // The receiver should not emit an detach as the connection was closed which implicitly
+        // detached the link.
+        if (close) {
+            receiver.close();
+        } else {
+            receiver.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsFlowWhenCreditSet() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond().withNextOutgoingId(42);
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100).withNextIncomingId(42);
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+        receiver.addCredit(100);
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsFlowWithNoIncomingIdWhenRemoteBeginHasNotArrivedYet() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin();
+        peer.expectAttach();
+        peer.expectFlow().withLinkCredit(100).withNextIncomingId(nullValue());
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open();
+
+        receiver.addCredit(100);
+
+        final CountDownLatch opened = new CountDownLatch(1);
+        receiver.openHandler((self) -> {
+            opened.countDown();
+        });
+
+        peer.waitForScriptToComplete();
+        peer.respondToLastBegin().withNextOutgoingId(42).now();
+        peer.respondToLastAttach().now();
+        peer.expectFlow().withLinkCredit(101).withNextIncomingId(42);
+        peer.expectDetach().respond();
+
+        assertTrue(opened.await(10, TimeUnit.SECONDS));
+
+        receiver.addCredit(1);
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsFlowAfterOpenedWhenCreditSetBeforeOpened() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.addCredit(100);
+        receiver.open();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsFlowAfterConnectionOpenFinallySent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+
+        // Create and open all resources except don't open the connection and then
+        // we will observe that the receiver flow doesn't fire until it has sent its
+        // attach following the session send its Begin.
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.addCredit(1);
+        receiver.open();
+
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(1);
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test //TODO: questionable. If its going to no-op the credit then it should perhaps not do this (open before parent) to begin with, as strange to send the attaches but not credit?
+    public void testReceiverOmitsFlowAfterConnectionOpenFinallySentWhenAfterDetached() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Create and open all resources except don't open the connection and then
+        // we will observe that the receiver flow doesn't fire since the link was
+        // detached prior to being able to send any state updates.
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.addCredit(1);
+        receiver.open();
+        receiver.detach();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(0, receiver.getCredit());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDrainAllOutstanding() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        int creditWindow = 100;
+
+        // Add some credit, verify not draining
+        Matcher<Boolean> notDrainingMatcher = anyOf(equalTo(false), nullValue());
+        peer.expectFlow().withDrain(notDrainingMatcher).withLinkCredit(creditWindow).withDeliveryCount(0);
+        receiver.addCredit(creditWindow);
+
+        peer.waitForScriptToComplete();
+
+        // Check that calling drain sends flow, and calls handler on response draining all credit
+        AtomicBoolean handlerCalled = new AtomicBoolean();
+        receiver.creditStateUpdateHandler(x -> {
+            handlerCalled.set(true);
+        });
+
+        peer.expectFlow().withDrain(true).withLinkCredit(creditWindow).withDeliveryCount(0)
+                         .respond()
+                         .withDrain(true).withLinkCredit(0).withDeliveryCount(creditWindow);
+
+        receiver.drain();
+
+        peer.waitForScriptToComplete();
+        assertTrue(handlerCalled.get(), "Handler was not called");
+
+        peer.expectDetach().respond();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDrainWithNoCreditResultInNoOutput() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().respond();
+
+        receiver.drain();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDrainAllowsOnlyOnePendingDrain() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        int creditWindow = 100;
+
+        // Add some credit, verify not draining
+        Matcher<Boolean> notDrainingMatcher = anyOf(equalTo(false), nullValue());
+        peer.expectFlow().withDrain(notDrainingMatcher).withLinkCredit(creditWindow).withDeliveryCount(0);
+        receiver.addCredit(creditWindow);
+
+        peer.waitForScriptToComplete();
+
+        peer.expectFlow().withDrain(true).withLinkCredit(creditWindow).withDeliveryCount(0);
+
+        receiver.drain();
+
+        assertThrows(IllegalStateException.class, () -> receiver.drain());
+
+        peer.waitForScriptToComplete();
+
+        peer.expectDetach().respond();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDrainWithCreditsAllowsOnlyOnePendingDrain() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open();
+
+        final int creditWindow = 100;
+
+        peer.waitForScriptToComplete();
+
+        peer.expectFlow().withDrain(true).withLinkCredit(creditWindow).withDeliveryCount(0);
+
+        receiver.drain(creditWindow);
+
+        assertThrows(IllegalStateException.class, () -> receiver.drain(creditWindow));
+
+        peer.waitForScriptToComplete();
+
+        peer.expectDetach().respond();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverThrowsOnAddCreditAfterConnectionClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+        connection.close();
+
+        try {
+            receiver.addCredit(100);
+            fail("Should not be able to add credit after connection was closed");
+        } catch (IllegalStateException ise) {
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverThrowsOnAddCreditAfterSessionClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+        session.close();
+
+        try {
+            receiver.addCredit(100);
+            fail("Should not be able to add credit after session was closed");
+        } catch (IllegalStateException ise) {
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDispatchesIncomingDelivery() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+        });
+        receiver.open();
+        receiver.addCredit(100);
+        receiver.close();
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Deliver should not be partial");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsDispostionForTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+
+            delivery.disposition(Accepted.getInstance(), true);
+        });
+        receiver.open();
+        receiver.addCredit(100);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Deliver should not be partial");
+        assertFalse(receiver.hasUnsettled());
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsDispostionOnlyOnceForTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+
+            delivery.disposition(Accepted.getInstance(), true);
+        });
+        receiver.open();
+        receiver.addCredit(100);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Deliver should not be partial");
+
+        // Already settled so this should trigger error
+        try {
+            receivedDelivery.get().disposition(Released.getInstance(), true);
+            fail("Should not be able to set a second disposition");
+        } catch (IllegalStateException ise) {
+            // Expected that we can't settle twice.
+        }
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsUpdatedDispostionsForTransferBeforeSettlement() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(false)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().released();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+
+            delivery.disposition(Accepted.getInstance(), false);
+        });
+        receiver.open();
+        receiver.addCredit(100);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Deliver should not be partial");
+        assertTrue(receiver.hasUnsettled());
+
+        // Second disposition should be sent as we didn't settle previously.
+        receivedDelivery.get().disposition(Released.getInstance(), true);
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverSendsUpdatedDispostionsForTransferBeforeSettlementThenSettles() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(false)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(false)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().released();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().released();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+
+            delivery.disposition(Accepted.getInstance());
+        });
+        receiver.open();
+        receiver.addCredit(100);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Deliver should not be partial");
+        assertTrue(receiver.hasUnsettled());
+
+        // Second disposition should be sent as we didn't settle previously.
+        receivedDelivery.get().disposition(Released.getInstance());
+        receivedDelivery.get().settle();
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    /**
+     * Verify that no Disposition frame is emitted by the Transport should a Delivery
+     * have disposition applied after the Close frame was sent.
+     */
+    @Test
+    public void testDispositionNoAllowedAfterCloseSent() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+        });
+
+        receiver.open();
+        receiver.addCredit(1);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Deliver should not be partial");
+
+        connection.close();
+
+        try {
+            receivedDelivery.get().disposition(Released.getInstance());
+            fail("Should not be able to set a disposition after the connection was closed");
+        } catch (IllegalStateException ise) {}
+
+        try {
+            receivedDelivery.get().disposition(Released.getInstance(), true);
+            fail("Should not be able to update a disposition after the connection was closed");
+        } catch (IllegalStateException ise) {}
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverReportsDeliveryUpdatedOnDisposition() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(100);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.remoteDisposition().withSettled(true)
+                                .withRole(Role.SENDER.getValue())
+                                .withState().accepted()
+                                .withFirst(0).queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+        });
+
+        final AtomicBoolean deliveryUpdatedAndSettled = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> updatedDelivery = new AtomicReference<>();
+        receiver.deliveryStateUpdatedHandler(delivery -> {
+            if (delivery.isRemotelySettled()) {
+                deliveryUpdatedAndSettled.set(true);
+            }
+
+            updatedDelivery.set(delivery);
+        });
+
+        receiver.open();
+        receiver.addCredit(100);
+        receiver.close();
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Delivery should not be partial");
+        assertFalse(updatedDelivery.get().isPartial(), "Delivery should not be partial");
+        assertTrue(deliveryUpdatedAndSettled.get(), "Delivery should have been updated to settled");
+        assertSame(receivedDelivery.get(), updatedDelivery.get(), "Delivery should be same object as first received");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverReportsDeliveryUpdatedOnDispositionForMultipleTransfers() throws Exception {
+        doTestReceiverReportsDeliveryUpdatedOnDispositionForMultipleTransfers(0);
+    }
+
+    @Test
+    public void testReceiverReportsDeliveryUpdatedOnDispositionForMultipleTransfersDeliveryIdOverflows() throws Exception {
+        doTestReceiverReportsDeliveryUpdatedOnDispositionForMultipleTransfers(Integer.MAX_VALUE);
+    }
+
+    private void doTestReceiverReportsDeliveryUpdatedOnDispositionForMultipleTransfers(int firstDeliveryId) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(firstDeliveryId)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.remoteTransfer().withDeliveryId(firstDeliveryId + 1)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.remoteDisposition().withSettled(true)
+                                .withRole(Role.SENDER.getValue())
+                                .withState().accepted()
+                                .withFirst(firstDeliveryId)
+                                .withLast(firstDeliveryId + 1).queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicInteger dispositionCounter = new AtomicInteger();
+
+        final ArrayList<IncomingDelivery> deliveries = new ArrayList<>();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+        });
+
+        receiver.deliveryStateUpdatedHandler(delivery -> {
+            if (delivery.isRemotelySettled()) {
+                dispositionCounter.incrementAndGet();
+                deliveries.add(delivery);
+            }
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+        receiver.close();
+
+        assertEquals(2, deliveryCounter.get(), "Not all deliveries arrived");
+        assertEquals(2, deliveries.size(), "Not all deliveries received dispositions");
+
+        byte deliveryTag = 0;
+
+        for (IncomingDelivery delivery : deliveries) {
+            assertEquals(deliveryTag++, delivery.getTag().tagBuffer().getByte(0), "Delivery not updated in correct order");
+            assertTrue(delivery.isRemotelySettled(), "Delivery should be marked as remotely setted");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverReportsDeliveryUpdatedNextFrameForMultiFrameTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        String text = "test-string-for-split-frame-delivery";
+        byte[] encoded = text.getBytes(StandardCharsets.UTF_8);
+        byte[] first = Arrays.copyOfRange(encoded, 0, encoded.length / 2);
+        byte[] second = Arrays.copyOfRange(encoded, encoded.length / 2, encoded.length);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withBody().withData(first).also().queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withBody().withData(second).also().queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        final AtomicInteger deliverReads = new AtomicInteger();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+            deliverReads.incrementAndGet();
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Delivery should not be partial");
+        assertEquals(2, deliverReads.get(), "Deliver should have been read twice for two transfers");
+        assertSame(receivedDelivery.get(), receivedDelivery.get(), "Delivery should be same object as first received");
+
+        ProtonBuffer payload = receivedDelivery.get().readAll();
+
+        assertNotNull(payload);
+
+        // We are cheating a bit here as this ins't how the encoding would normally work.
+        Data section1 = decoder.readObject(payload, decoderState, Data.class);
+        Data section2 = decoder.readObject(payload, decoderState, Data.class);
+
+        Binary data1 = section1.getBinary();
+        Binary data2 = section2.getBinary();
+
+        ProtonBuffer combined = ProtonByteBufferAllocator.DEFAULT.allocate(encoded.length);
+
+        combined.writeBytes(data1.asByteBuffer());
+        combined.writeBytes(data2.asByteBuffer());
+
+        assertEquals(text, combined.toString(StandardCharsets.UTF_8), "Encoded and Decoded strings don't match");
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverReportsUpdateWhenLastFrameOfMultiFrameTransferHasNoPayload() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        String text = "test-string-for-split-frame-delivery";
+        byte[] encoded = text.getBytes(StandardCharsets.UTF_8);
+        byte[] first = Arrays.copyOfRange(encoded, 0, encoded.length / 2);
+        byte[] second = Arrays.copyOfRange(encoded, encoded.length / 2, encoded.length);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withBody().withData(first).also().queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withBody().withData(second).also().queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicBoolean deliveryArrived = new AtomicBoolean();
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        final AtomicInteger deliverReads = new AtomicInteger();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.set(true);
+            receivedDelivery.set(delivery);
+            deliverReads.incrementAndGet();
+        });
+
+        receiver.open();
+        receiver.addCredit(1);
+
+        assertTrue(deliveryArrived.get(), "Delivery did not arrive at the receiver");
+        assertFalse(receivedDelivery.get().isPartial(), "Delivery should not be partial");
+        assertEquals(4, deliverReads.get(), "Deliver should have been read twice for two transfers");
+        assertSame(receivedDelivery.get(), receivedDelivery.get(), "Delivery should be same object as first received");
+
+        ProtonBuffer payload = receivedDelivery.get().readAll();
+
+        assertNotNull(payload);
+
+        // We are cheating a bit here as this ins't how the encoding would normally work.
+        Data section1 = decoder.readObject(payload, decoderState, Data.class);
+        Data section2 = decoder.readObject(payload, decoderState, Data.class);
+
+        Binary data1 = section1.getBinary();
+        Binary data2 = section2.getBinary();
+
+        ProtonBuffer combined = ProtonByteBufferAllocator.DEFAULT.allocate(encoded.length);
+
+        combined.writeBytes(data1.asByteBuffer());
+        combined.writeBytes(data2.asByteBuffer());
+
+        assertEquals(text, combined.toString(StandardCharsets.UTF_8), "Encoded and Decoded strings don't match");
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testMultiplexMultiFrameDeliveriesOnSingleSessionIncoming() throws Exception {
+        doMultiplexMultiFrameDeliveryOnSingleSessionIncomingTestImpl(true);
+    }
+
+    @Test
+    public void testMultiplexMultiFrameDeliveryOnSingleSessionIncoming() throws Exception {
+        doMultiplexMultiFrameDeliveryOnSingleSessionIncomingTestImpl(false);
+    }
+
+    private void doMultiplexMultiFrameDeliveryOnSingleSessionIncomingTestImpl(boolean bothDeliveriesMultiFrame) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("receiver-1").respond();
+        peer.expectAttach().withHandle(1).withName("receiver-2").respond();
+        peer.expectFlow().withHandle(0).withLinkCredit(5);
+        peer.expectFlow().withHandle(1).withLinkCredit(5);
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver1 = session.receiver("receiver-1");
+        Receiver receiver2 = session.receiver("receiver-2");
+
+        final String delivery1LinkedResource = "Delivery1";
+        final String delivery2LinkedResource = "Delivery2";
+
+        final AtomicReference<IncomingDelivery> receivedDelivery1 = new AtomicReference<>();
+        final AtomicReference<IncomingDelivery> receivedDelivery2 = new AtomicReference<>();
+
+        final AtomicBoolean delivery1Updated = new AtomicBoolean();
+        final AtomicBoolean delivery2Updated = new AtomicBoolean();
+
+        final String deliveryTag1 = "tag1";
+        final String deliveryTag2 = "tag2";
+
+        final byte[] payload1 = new byte[] { 1, 1 };
+        final byte[] payload2 = new byte[] { 2, 2 };
+
+        // Receiver 1 handlers for delivery processing.
+        receiver1.deliveryReadHandler(delivery -> {
+            receivedDelivery1.set(delivery);
+            delivery.setLinkedResource(delivery1LinkedResource);
+        });
+        receiver1.deliveryStateUpdatedHandler(delivery -> {
+            delivery1Updated.set(true);
+            assertEquals(delivery1LinkedResource, delivery.getLinkedResource());
+            assertEquals(delivery1LinkedResource, delivery.getLinkedResource(String.class));
+            final String autoCasted = delivery.getLinkedResource();
+            assertEquals(delivery1LinkedResource, autoCasted);
+        });
+
+        // Receiver 2 handlers for delivery processing.
+        receiver2.deliveryReadHandler(delivery -> {
+            receivedDelivery2.set(delivery);
+            delivery.setLinkedResource(delivery2LinkedResource);
+        });
+        receiver2.deliveryStateUpdatedHandler(delivery -> {
+            delivery2Updated.set(true);
+            assertEquals(delivery2LinkedResource, delivery.getLinkedResource());
+            assertEquals(delivery2LinkedResource, delivery.getLinkedResource(String.class));
+            final String autoCasted = delivery.getLinkedResource();
+            assertEquals(delivery2LinkedResource, autoCasted);
+        });
+
+        receiver1.open();
+        receiver2.open();
+
+        receiver1.addCredit(5);
+        receiver2.addCredit(5);
+
+        assertNull(receivedDelivery1.get(), "Should not have any delivery data yet on receiver 1");
+        assertNull(receivedDelivery2.get(), "Should not have any delivery date yet on receiver 2");
+
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withHandle(0)
+                             .withDeliveryTag(deliveryTag1.getBytes(StandardCharsets.UTF_8))
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(payload1).now();
+        peer.remoteTransfer().withDeliveryId(1)
+                             .withHandle(1)
+                             .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                             .withMore(bothDeliveriesMultiFrame)
+                             .withMessageFormat(0)
+                             .withPayload(payload2).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have a delivery event on receiver 1");
+        assertNotNull(receivedDelivery2.get(), "Should have a delivery event on receiver 2");
+
+        assertTrue(receivedDelivery1.get().isPartial(), "Delivery on Receiver 1 Should not be complete");
+        if (bothDeliveriesMultiFrame) {
+            assertTrue(receivedDelivery2.get().isPartial(), "Delivery on Receiver 2 Should be complete");
+        } else {
+            assertFalse(receivedDelivery2.get().isPartial(), "Delivery on Receiver 2 Should not be complete");
+        }
+
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withHandle(0)
+                             .withDeliveryTag(deliveryTag1.getBytes(StandardCharsets.UTF_8))
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload1).now();
+        if (bothDeliveriesMultiFrame) {
+            peer.remoteTransfer().withDeliveryId(1)
+                                 .withHandle(1)
+                                 .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                                 .withMore(false)
+                                 .withMessageFormat(0)
+                                 .withPayload(payload2).now();
+        }
+
+        assertFalse(receivedDelivery1.get().isPartial(), "Delivery on Receiver 1 Should be complete");
+        assertFalse(receivedDelivery2.get().isPartial(), "Delivery on Receiver 2 Should be complete");
+
+        peer.expectDisposition().withFirst(1)
+                                .withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted();
+
+        assertArrayEquals(deliveryTag1.getBytes(StandardCharsets.UTF_8), receivedDelivery1.get().getTag().tagBuffer().getArray());
+        assertArrayEquals(deliveryTag2.getBytes(StandardCharsets.UTF_8), receivedDelivery2.get().getTag().tagBuffer().getArray());
+
+        ProtonBuffer payloadBuffer1 = receivedDelivery1.get().readAll();
+        ProtonBuffer payloadBuffer2 = receivedDelivery2.get().readAll();
+
+        assertEquals(payload1.length * 2, payloadBuffer1.getReadableBytes(), "Received 1 payload size is wrong");
+        assertEquals(payload2.length * (bothDeliveriesMultiFrame ? 2 : 1), payloadBuffer2.getReadableBytes(), "Received 2 payload size is wrong");
+
+        receivedDelivery2.get().disposition(Accepted.getInstance(), true);
+        receivedDelivery1.get().disposition(Accepted.getInstance(), true);
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDeliveryIdTrackingHandlesAbortedDelivery() {
+        // Check aborted=true, more=false, settled=true.
+        doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(false, true);
+        // Check aborted=true, more=false, settled=unset(false)
+        // Aborted overrides settled not being set.
+        doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(false, null);
+        // Check aborted=true, more=false, settled=false
+        // Aborted overrides settled being explicitly false.
+        doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(false, false);
+        // Check aborted=true, more=true, settled=true
+        // Aborted overrides the more=true.
+        doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(true, true);
+        // Check aborted=true, more=true, settled=unset(false)
+        // Aborted overrides the more=true, and settled being unset.
+        doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(true, null);
+        // Check aborted=true, more=true, settled=false
+        // Aborted overrides the more=true, and settled explicitly false.
+        doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(true, false);
+    }
+
+    private void doTestReceiverDeliveryIdTrackingHandlesAbortedDelivery(boolean setMoreOnAbortedTransfer, Boolean setSettledOnAbortedTransfer) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("receiver");
+        receiver.addCredit(2);
+
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        final AtomicReference<IncomingDelivery> abortedDelivery = new AtomicReference<>();
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicBoolean deliveryUpdated = new AtomicBoolean();
+        final byte[] payload = new byte[] { 1 };
+
+        // Receiver 1 handlers for delivery processing.
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+            if (delivery.isAborted()) {
+                abortedDelivery.set(delivery);
+            } else {
+                receivedDelivery.set(delivery);
+            }
+        });
+        receiver.deliveryStateUpdatedHandler(delivery -> {
+            deliveryUpdated.set(true);
+        });
+
+        receiver.open();
+
+        assertNull(receivedDelivery.get(), "Should not have any delivery data yet on receiver 1");
+        assertEquals(0, deliveryCounter.get(), "Should not have any delivery data yet on receiver 1");
+        assertFalse(deliveryUpdated.get(), "Should not have any delivery data yet on receiver 1");
+
+        // First chunk indicates more to come.
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(payload).now();
+
+        assertNotNull(receivedDelivery.get(), "Should have delivery data on receiver");
+        assertEquals(1, deliveryCounter.get(), "Should have delivery data on receiver");
+        assertFalse(deliveryUpdated.get(), "Should not have any delivery updates yet on receiver");
+
+        // Second chunk indicates more to come as a twist but also signals aborted.
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withSettled(setSettledOnAbortedTransfer)
+                             .withMore(setMoreOnAbortedTransfer)
+                             .withAborted(true)
+                             .withMessageFormat(0)
+                             .withPayload(payload).now();
+
+        assertNotNull(receivedDelivery.get(), "Should have delivery data on receiver");
+        assertEquals(2, deliveryCounter.get(), "Should have delivery data on receiver");
+        assertFalse(deliveryUpdated.get(), "Should not have a delivery updates on receiver");
+        assertTrue(receivedDelivery.get().isAborted(), "Should now show that delivery is aborted");
+        assertTrue(receivedDelivery.get().isRemotelySettled(), "Should now show that delivery is remotely settled");
+        assertNull(receivedDelivery.get().readAll(), "Aboarted Delivery should discard read bytes");
+
+        // Another delivery now which should arrive just fine, no further frames on this one.
+        peer.remoteTransfer().withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).now();
+
+        assertNotNull(abortedDelivery.get(), "Should have one aborted delivery");
+        assertNotNull(receivedDelivery.get(), "Should have delivery data on receiver");
+        assertNotSame(abortedDelivery.get(), receivedDelivery.get(), "Should have a final non-aborted delivery");
+        assertEquals(3, deliveryCounter.get(), "Should have delivery data on receiver");
+        assertFalse(deliveryUpdated.get(), "Should not have a delivery updates on receiver");
+        assertFalse(receivedDelivery.get().isAborted(), "Should now show that delivery is not aborted");
+        assertEquals(2, receivedDelivery.get().getTag().tagBuffer().getByte(0), "Should have delivery tagged as two");
+
+        // Test that delivery count updates correctly on next flow
+        peer.expectFlow().withLinkCredit(10).withDeliveryCount(2);
+
+        receiver.addCredit(10);
+
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testAbortedTransferRemovedFromUnsettledListOnceSettledRemoteSettles() {
+        doTestAbortedTransferRemovedFromUnsettledListOnceSettled(true);
+    }
+
+    @Test
+    public void testAbortedTransferRemovedFromUnsettledListOnceSettledRemoteDoesNotSettle() {
+        doTestAbortedTransferRemovedFromUnsettledListOnceSettled(false);
+    }
+
+    private void doTestAbortedTransferRemovedFromUnsettledListOnceSettled(boolean remoteSettled) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(1);
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("receiver");
+        receiver.addCredit(1);
+
+        final AtomicReference<IncomingDelivery> abortedDelivery = new AtomicReference<>();
+        final byte[] payload = new byte[] { 1 };
+
+        // Receiver 1 handlers for delivery processing.
+        receiver.deliveryReadHandler(delivery -> {
+            if (delivery.isAborted()) {
+                abortedDelivery.set(delivery);
+            }
+        });
+
+        receiver.open();
+
+        // Send one chunk then abort to check that local side can settle and clear
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(payload).now();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withSettled(remoteSettled)
+                             .withMore(false)
+                             .withAborted(true)
+                             .withMessageFormat(0)
+                             .withPayload(payload).now();
+
+        assertNotNull(abortedDelivery.get(), "should have one aborted delivery");
+
+        assertTrue(receiver.hasUnsettled());
+        assertEquals(1, receiver.unsettled().size());
+        abortedDelivery.get().settle();
+        assertFalse(receiver.hasUnsettled());
+        assertEquals(0, receiver.unsettled().size());
+
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDeliveryWithIdOmittedOnContinuationTransfers() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("receiver-1").respond();
+        peer.expectAttach().withHandle(1).withName("receiver-2").respond();
+        peer.expectFlow().withHandle(0).withLinkCredit(5);
+        peer.expectFlow().withHandle(1).withLinkCredit(5);
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver1 = session.receiver("receiver-1");
+        Receiver receiver2 = session.receiver("receiver-2");
+
+        final AtomicReference<IncomingDelivery> receivedDelivery1 = new AtomicReference<>();
+        final AtomicReference<IncomingDelivery> receivedDelivery2 = new AtomicReference<>();
+
+        final AtomicInteger receiver1Transfers = new AtomicInteger();
+        final AtomicInteger receiver2Transfers = new AtomicInteger();
+
+        final AtomicBoolean delivery1Updated = new AtomicBoolean();
+        final AtomicBoolean delivery2Updated = new AtomicBoolean();
+
+        final String deliveryTag1 = "tag1";
+        final String deliveryTag2 = "tag2";
+
+        // Receiver 1 handlers for delivery processing.
+        receiver1.deliveryReadHandler(delivery -> {
+            receivedDelivery1.set(delivery);
+            receiver1Transfers.incrementAndGet();
+        });
+        receiver1.deliveryStateUpdatedHandler(delivery -> {
+            delivery1Updated.set(true);
+            receiver1Transfers.incrementAndGet();
+        });
+
+        // Receiver 2 handlers for delivery processing.
+        receiver2.deliveryReadHandler(delivery -> {
+            receivedDelivery2.set(delivery);
+            receiver2Transfers.incrementAndGet();
+        });
+        receiver2.deliveryStateUpdatedHandler(delivery -> {
+            delivery2Updated.set(true);
+            receiver2Transfers.incrementAndGet();
+        });
+
+        receiver1.open();
+        receiver2.open();
+
+        receiver1.addCredit(5);
+        receiver2.addCredit(5);
+
+        assertNull(receivedDelivery1.get(), "Should not have any delivery data yet on receiver 1");
+        assertNull(receivedDelivery2.get(), "Should not have any delivery date yet on receiver 2");
+        assertEquals(0, receiver1Transfers.get(), "Receiver 1 should not have any transfers yet");
+        assertEquals(0, receiver2Transfers.get(), "Receiver 2 should not have any transfers yet");
+
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withHandle(0)
+                             .withDeliveryTag(deliveryTag1.getBytes(StandardCharsets.UTF_8))
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {1}).now();
+        peer.remoteTransfer().withDeliveryId(1)
+                             .withHandle(1)
+                             .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {10}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have a delivery event on receiver 1");
+        assertNotNull(receivedDelivery2.get(), "Should have a delivery event on receiver 2");
+        assertEquals(1, receiver1Transfers.get(), "Receiver 1 should have 1 transfers");
+        assertEquals(1, receiver2Transfers.get(), "Receiver 2 should have 1 transfers");
+        assertNotSame(receivedDelivery1.get(), receivedDelivery2.get());
+
+        peer.remoteTransfer().withHandle(1)
+                             .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {11}).now();
+        peer.remoteTransfer().withHandle(0)
+                             .withDeliveryTag(deliveryTag1.getBytes(StandardCharsets.UTF_8))
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {2}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have a delivery event on receiver 1");
+        assertNotNull(receivedDelivery2.get(), "Should have a delivery event on receiver 2");
+        assertEquals(2, receiver1Transfers.get(), "Receiver 1 should have 2 transfers");
+        assertEquals(2, receiver2Transfers.get(), "Receiver 2 should have 2 transfers");
+        assertNotSame(receivedDelivery1.get(), receivedDelivery2.get());
+
+        peer.remoteTransfer().withHandle(0)
+                             .withDeliveryTag(deliveryTag1.getBytes(StandardCharsets.UTF_8))
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {3}).now();
+        peer.remoteTransfer().withHandle(1)
+                             .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {12}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have a delivery event on receiver 1");
+        assertNotNull(receivedDelivery2.get(), "Should have a delivery event on receiver 2");
+        assertEquals(3, receiver1Transfers.get(), "Receiver 1 should have 3 transfers");
+        assertEquals(3, receiver2Transfers.get(), "Receiver 2 should have 3 transfers");
+        assertNotSame(receivedDelivery1.get(), receivedDelivery2.get());
+
+        peer.remoteTransfer().withHandle(1)
+                             .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {13}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have a delivery event on receiver 1");
+        assertNotNull(receivedDelivery2.get(), "Should have a delivery event on receiver 2");
+        assertEquals(3, receiver1Transfers.get(), "Receiver 1 should have 3 transfers");
+        assertEquals(4, receiver2Transfers.get(), "Receiver 2 should have 4 transfers");
+        assertNotSame(receivedDelivery1.get(), receivedDelivery2.get());
+        assertFalse(receivedDelivery1.get().isPartial(), "Delivery on Receiver 1 Should be complete");
+        assertFalse(receivedDelivery2.get().isPartial(), "Delivery on Receiver 2 Should be complete");
+
+        assertArrayEquals(deliveryTag1.getBytes(StandardCharsets.UTF_8), receivedDelivery1.get().getTag().tagBuffer().getArray());
+        assertArrayEquals(deliveryTag2.getBytes(StandardCharsets.UTF_8), receivedDelivery2.get().getTag().tagBuffer().getArray());
+
+        ProtonBuffer delivery1Buffer = receivedDelivery1.get().readAll();
+        ProtonBuffer delivery2Buffer = receivedDelivery2.get().readAll();
+
+        for (int i = 1; i < 4; ++i) {
+            assertEquals(i, delivery1Buffer.readByte());
+        }
+
+        for (int i = 10; i < 14; ++i) {
+            assertEquals(i, delivery2Buffer.readByte());
+        }
+
+        assertNull(receivedDelivery1.get().readAll());
+        assertNull(receivedDelivery2.get().readAll());
+
+        peer.expectDetach().withHandle(0).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        receiver1.close();
+        receiver2.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDeliveryIdThresholdsAndWraps() {
+        // Check start from 0
+        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.ZERO, UnsignedInteger.ONE, UnsignedInteger.valueOf(2));
+        // Check run up to max-int (interesting boundary for underlying impl)
+        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.valueOf(Integer.MAX_VALUE - 2), UnsignedInteger.valueOf(Integer.MAX_VALUE -1), UnsignedInteger.valueOf(Integer.MAX_VALUE));
+        // Check crossing from signed range value into unsigned range value (interesting boundary for underlying impl)
+        long maxIntAsLong = Integer.MAX_VALUE;
+        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.valueOf(maxIntAsLong), UnsignedInteger.valueOf(maxIntAsLong + 1L), UnsignedInteger.valueOf(maxIntAsLong + 2L));
+        // Check run up to max-uint
+        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.valueOf(0xFFFFFFFFL - 2), UnsignedInteger.valueOf(0xFFFFFFFFL - 1), UnsignedInteger.MAX_VALUE);
+        // Check wrapping from max unsigned value back to min(/0).
+        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.MAX_VALUE, UnsignedInteger.ZERO, UnsignedInteger.ONE);
+    }
+
+    private void doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger deliveryId1, UnsignedInteger deliveryId2, UnsignedInteger deliveryId3) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond().withNextOutgoingId(deliveryId1.intValue());
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(5);
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("receiver");
+
+        final AtomicReference<IncomingDelivery> receivedDelivery1 = new AtomicReference<>();
+        final AtomicReference<IncomingDelivery> receivedDelivery2 = new AtomicReference<>();
+        final AtomicReference<IncomingDelivery> receivedDelivery3 = new AtomicReference<>();
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+
+        final String deliveryTag1 = "tag1";
+        final String deliveryTag2 = "tag2";
+        final String deliveryTag3 = "tag3";
+
+        // Receiver handlers for delivery processing.
+        receiver.deliveryReadHandler(delivery -> {
+            switch (deliveryCounter.get()) {
+                case 0:
+                    receivedDelivery1.set(delivery);
+                    break;
+                case 1:
+                    receivedDelivery2.set(delivery);
+                    break;
+                case 2:
+                    receivedDelivery3.set(delivery);
+                    break;
+                default:
+                    break;
+            }
+            deliveryCounter.incrementAndGet();
+        });
+        receiver.deliveryStateUpdatedHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+        });
+
+        receiver.open();
+        receiver.addCredit(5);
+
+        assertNull(receivedDelivery1.get(), "Should not have received delivery 1");
+        assertNull(receivedDelivery2.get(), "Should not have received delivery 2");
+        assertNull(receivedDelivery3.get(), "Should not have received delivery 3");
+        assertEquals(0, deliveryCounter.get(), "Receiver should not have any deliveries yet");
+
+        peer.remoteTransfer().withDeliveryId(deliveryId1.intValue())
+                             .withDeliveryTag(deliveryTag1.getBytes(StandardCharsets.UTF_8))
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {1}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have received delivery 1");
+        assertNull(receivedDelivery2.get(), "Should not have received delivery 2");
+        assertNull(receivedDelivery3.get(), "Should not have received delivery 3");
+        assertEquals(1, deliveryCounter.get(), "Receiver should have 1 deliveries now");
+
+        peer.remoteTransfer().withDeliveryId(deliveryId2.intValue())
+                             .withDeliveryTag(deliveryTag2.getBytes(StandardCharsets.UTF_8))
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {2}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have received delivery 1");
+        assertNotNull(receivedDelivery2.get(), "Should have received delivery 2");
+        assertNull(receivedDelivery3.get(), "Should not have received delivery 3");
+        assertEquals(2, deliveryCounter.get(), "Receiver should have 2 deliveries now");
+
+        peer.remoteTransfer().withDeliveryId(deliveryId3.intValue())
+                             .withDeliveryTag(deliveryTag3.getBytes(StandardCharsets.UTF_8))
+                             .withMessageFormat(0)
+                             .withPayload(new byte[] {3}).now();
+
+        assertNotNull(receivedDelivery1.get(), "Should have received delivery 1");
+        assertNotNull(receivedDelivery2.get(), "Should have received delivery 2");
+        assertNotNull(receivedDelivery3.get(), "Should have received delivery 3");
+        assertEquals(3, deliveryCounter.get(), "Receiver should have 3 deliveries now");
+
+        assertNotSame(receivedDelivery1.get(), receivedDelivery2.get(), "delivery duplicate detected");
+        assertNotSame(receivedDelivery2.get(), receivedDelivery3.get(), "delivery duplicate detected");
+        assertNotSame(receivedDelivery1.get(), receivedDelivery3.get(), "delivery duplicate detected");
+
+        // Verify deliveries arrived with expected payload
+        assertArrayEquals(deliveryTag1.getBytes(StandardCharsets.UTF_8), receivedDelivery1.get().getTag().tagBuffer().getArray());
+        assertArrayEquals(deliveryTag2.getBytes(StandardCharsets.UTF_8), receivedDelivery2.get().getTag().tagBuffer().getArray());
+        assertArrayEquals(deliveryTag3.getBytes(StandardCharsets.UTF_8), receivedDelivery3.get().getTag().tagBuffer().getArray());
+
+        ProtonBuffer delivery1Buffer = receivedDelivery1.get().readAll();
+        ProtonBuffer delivery2Buffer = receivedDelivery2.get().readAll();
+        ProtonBuffer delivery3Buffer = receivedDelivery3.get().readAll();
+
+        assertEquals(1, delivery1Buffer.readByte(), "Delivery 1 payload not as expected");
+        assertEquals(2, delivery2Buffer.readByte(), "Delivery 2 payload not as expected");
+        assertEquals(3, delivery3Buffer.readByte(), "Delivery 3 payload not as expected");
+
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverFlowSentAfterAttachWrittenWhenCreditPrefilled() throws Exception {
+        doTestReceiverFlowSentAfterAttachWritten(true);
+    }
+
+    @Test
+    public void testReceiverFlowSentAfterAttachWrittenWhenCreditAddedBeforeAttachResponse() throws Exception {
+        doTestReceiverFlowSentAfterAttachWritten(false);
+    }
+
+    private void doTestReceiverFlowSentAfterAttachWritten(boolean creditBeforeOpen) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+
+        Receiver receiver = session.receiver("receiver");
+
+        if (creditBeforeOpen) {
+            // Add credit before open, no frame should be written until opened.
+            receiver.addCredit(5);
+        }
+
+        // Expect attach but don't respond to observe that flow is sent regardless.
+        peer.waitForScriptToComplete();
+        peer.expectAttach();
+        peer.expectFlow().withLinkCredit(5).withDeliveryCount(nullValue());
+
+        receiver.open();
+
+        if (!creditBeforeOpen) {
+            // Add credit after open, frame should be written regardless of no attach response
+            receiver.addCredit(5);
+        }
+
+        peer.respondToLastAttach().now();
+        peer.waitForScriptToComplete();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        receiver.detach();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverHandlesDeferredOpenAndBeginAttachResponses() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.expectBegin();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withDynamic(true)
+                           .withAddress((String) null);
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("receiver-1");
+        receiver.setSource(new Source().setDynamic(true).setAddress(null));
+        receiver.openHandler(result -> receiverRemotelyOpened.set(true)).open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        // This should happen after we inject the held open and attach
+        peer.expectClose().respond();
+
+        // Inject held responses to get the ball rolling again
+        peer.remoteOpen().withOfferedCapabilities("ANONYMOUS_REALY").now();
+        peer.respondToLastBegin().now();
+        peer.respondToLastAttach().now();
+
+        assertTrue(receiverRemotelyOpened.get(), "Receiver remote opened event did not fire");
+        assertNotNull(receiver.getRemoteSource().getAddress());
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, true, true);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndNoRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenAndResponseBeginWrittenAndNoRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, false, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, false, false, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenNotWritten() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(false, false, false, false);
+    }
+
+    private void testCloseAfterShutdownNoOutputAndNoException(boolean respondToHeader, boolean respondToOpen, boolean respondToBegin, boolean respondToAttach) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                if (respondToBegin) {
+                    peer.expectBegin().respond();
+                    if (respondToAttach) {
+                        peer.expectAttach().respond();
+                    } else {
+                        peer.expectAttach();
+                    }
+                } else {
+                    peer.expectBegin();
+                    peer.expectAttach();
+                }
+            } else {
+                peer.expectOpen();
+                peer.expectBegin();
+                peer.expectAttach();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        engine.shutdown();
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        receiver.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndReponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true, true);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenAndResponseBeginWrittenAndNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, false, false, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenNotWritten() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(false, false, false, false);
+    }
+
+    private void testCloseAfterEngineFailedThrowsAndNoOutputWritten(boolean respondToHeader, boolean respondToOpen, boolean respondToBegin, boolean respondToAttach) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                if (respondToBegin) {
+                    peer.expectBegin().respond();
+                    if (respondToAttach) {
+                        peer.expectAttach().respond();
+                    } else {
+                        peer.expectAttach();
+                    }
+                } else {
+                    peer.expectBegin();
+                    peer.expectAttach();
+                }
+                peer.expectClose();
+            } else {
+                peer.expectOpen();
+                peer.expectBegin();
+                peer.expectAttach();
+                peer.expectClose();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        engine.engineFailed(new IOException());
+
+        try {
+            receiver.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        try {
+            session.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        try {
+            connection.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        engine.shutdown();  // Explicit shutdown now allows local close to complete
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        receiver.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testCloseReceiverWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(true);
+    }
+
+    @Test
+    public void testDetachReceiverWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(false);
+    }
+
+    public void doTestCloseOrDetachWithErrorCondition(boolean close) throws Exception {
+        final String condition = "amqp:link:detach-forced";
+        final String description = "something bad happened.";
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().withClosed(close)
+                           .withError(condition, description)
+                           .respond();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Receiver receiver = session.receiver("receiver-1");
+        receiver.open();
+        receiver.setCondition(new ErrorCondition(Symbol.valueOf(condition), description));
+
+        if (close) {
+            receiver.close();
+        } else {
+            receiver.detach();
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterReceiverLocallyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenLinkIsNotOperable(true, false, false);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterReceiverLocallyDetached() throws Exception {
+        doTestReceiverAddCreditFailsWhenLinkIsNotOperable(true, false, true);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterReceiverRemotelyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenLinkIsNotOperable(false, true, false);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterReceiverRemotelyDetached() throws Exception {
+        doTestReceiverAddCreditFailsWhenLinkIsNotOperable(false, true, true);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterReceiverFullyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenLinkIsNotOperable(true, true, false);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterReceiverFullyDetached() throws Exception {
+        doTestReceiverAddCreditFailsWhenLinkIsNotOperable(true, true, true);
+    }
+
+    private void doTestReceiverAddCreditFailsWhenLinkIsNotOperable(boolean localClose, boolean remoteClose, boolean detach) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        if (localClose) {
+            if (remoteClose) {
+                peer.expectDetach().respond();
+            } else {
+                peer.expectDetach();
+            }
+
+            if (detach) {
+                receiver.detach();
+            } else {
+                receiver.close();
+            }
+        } else if (remoteClose) {
+            peer.remoteDetach().withClosed(!detach).now();
+        }
+
+        try {
+            receiver.addCredit(2);
+            fail("Receiver should not allow addCredit to be called");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterSessionLocallyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenSessionNotOperable(true, false);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterSessionRemotelyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenSessionNotOperable(false, true);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterSessionFullyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenSessionNotOperable(true, true);
+    }
+
+    private void doTestReceiverAddCreditFailsWhenSessionNotOperable(boolean localClose, boolean remoteClose) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        if (localClose) {
+            if (remoteClose) {
+                peer.expectEnd().respond();
+            } else {
+                peer.expectEnd();
+            }
+
+            session.close();
+        } else if (remoteClose) {
+            peer.remoteEnd().now();
+        }
+
+        try {
+            receiver.addCredit(2);
+            fail("Receiver should not allow addCredit to be called");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterConnectionLocallyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenConnectionNotOperable(true, false);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterConnectionRemotelyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenConnectionNotOperable(false, true);
+    }
+
+    @Test
+    public void testReceiverAddCreditFailsAfterConnectionFullyClosed() throws Exception {
+        doTestReceiverAddCreditFailsWhenConnectionNotOperable(true, true);
+    }
+
+    private void doTestReceiverAddCreditFailsWhenConnectionNotOperable(boolean localClose, boolean remoteClose) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        if (localClose) {
+            if (remoteClose) {
+                peer.expectClose().respond();
+            } else {
+                peer.expectClose();
+            }
+
+            connection.close();
+        } else if (remoteClose) {
+            peer.remoteClose().now();
+        }
+
+        try {
+            receiver.addCredit(2);
+            fail("Receiver should not allow addCredit to be called");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDispositionFailsAfterReceiverLocallyClosed() throws Exception {
+        doTestReceiverDispositionFailsWhenLinkIsNotOperable(true, false, false);
+    }
+
+    @Test
+    public void testReceiverDispositionFailsAfterReceiverLocallyDetached() throws Exception {
+        doTestReceiverDispositionFailsWhenLinkIsNotOperable(true, false, true);
+    }
+
+    @Test
+    public void testReceiverDispositionFailsAfterReceiverRemotelyClosed() throws Exception {
+        doTestReceiverDispositionFailsWhenLinkIsNotOperable(false, true, false);
+    }
+
+    @Test
+    public void testReceiverDispositionFailsAfterReceiverRemotelyDetached() throws Exception {
+        doTestReceiverDispositionFailsWhenLinkIsNotOperable(false, true, true);
+    }
+
+    @Test
+    public void testReceiverDispositionFailsAfterReceiverFullyClosed() throws Exception {
+        doTestReceiverDispositionFailsWhenLinkIsNotOperable(true, true, false);
+    }
+
+    @Test
+    public void testReceiverDispositionFailsAfterReceiverFullyDetached() throws Exception {
+        doTestReceiverDispositionFailsWhenLinkIsNotOperable(true, true, true);
+    }
+
+    private void doTestReceiverDispositionFailsWhenLinkIsNotOperable(boolean localClose, boolean remoteClose, boolean detach) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        // Should no-op with no deliveries
+        receiver.disposition(delivery -> true, Accepted.getInstance(), true);
+
+        if (localClose) {
+            if (remoteClose) {
+                peer.expectDetach().respond();
+            } else {
+                peer.expectDetach();
+            }
+
+            if (detach) {
+                receiver.detach();
+            } else {
+                receiver.close();
+            }
+        } else if (remoteClose) {
+            peer.remoteDetach().withClosed(!detach).now();
+        }
+
+        try {
+            receiver.disposition(delivery -> true, Accepted.getInstance(), true);
+            fail("Receiver should not allow dispotiion to be called");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDrainCreditAmountLessThanCurrentCreditThrowsIAE() throws Exception {
+        doTestReceiverDrainThrowsIAEForCertainDrainAmountScenarios(10, 1);
+    }
+
+    @Test
+    public void testDrainOfNegativeCreditAmountThrowsIAEWhenCreditIsZero() throws Exception {
+        doTestReceiverDrainThrowsIAEForCertainDrainAmountScenarios(0, -1);
+    }
+
+    @Test
+    public void testDrainOfNegativeCreditAmountThrowsIAEWhenCreditIsNotZero() throws Exception {
+        doTestReceiverDrainThrowsIAEForCertainDrainAmountScenarios(10, -1);
+    }
+
+    private void doTestReceiverDrainThrowsIAEForCertainDrainAmountScenarios(int credit, int drain) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        if (credit > 0) {
+            peer.expectFlow().withDrain(false).withLinkCredit(credit);
+        }
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open().addCredit(credit);
+
+        peer.waitForScriptToComplete();
+
+        // Check that calling drain sends flow, and calls handler on response draining all credit
+        AtomicBoolean handlerCalled = new AtomicBoolean();
+        receiver.creditStateUpdateHandler(x -> {
+            handlerCalled.set(true);
+        });
+
+        try {
+            receiver.drain(drain);
+            fail("Should not be able to drain given amount");
+        } catch (IllegalArgumentException iae) {}
+
+        peer.waitForScriptToComplete();
+        assertFalse(handlerCalled.get(), "Handler was called when no flow expected");
+
+        peer.expectDetach().respond();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDrainRequestWithNoCreditPendingAndAmountRequestedAsZero() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+
+        // Check that calling drain sends flow, and calls handler on response draining all credit
+        AtomicBoolean handlerCalled = new AtomicBoolean();
+        receiver.creditStateUpdateHandler(x -> {
+            handlerCalled.set(true);
+        });
+
+        assertFalse(receiver.drain(0));
+
+        peer.waitForScriptToComplete();
+        assertFalse(handlerCalled.get(), "Handler was not called");
+
+        peer.expectDetach().respond();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverDrainWithCreditsWhenNoCreditOutstanding() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+
+        final int drainAmount = 100;
+
+        // Check that calling drain sends flow, and calls handler on response draining all credit
+        AtomicBoolean handlerCalled = new AtomicBoolean();
+        receiver.creditStateUpdateHandler(x -> {
+            handlerCalled.set(true);
+        });
+
+        peer.expectFlow().withDrain(true).withLinkCredit(drainAmount).withDeliveryCount(0)
+                         .respond()
+                         .withDrain(true).withLinkCredit(0).withDeliveryCount(drainAmount);
+
+        receiver.drain(drainAmount);
+
+        peer.waitForScriptToComplete();
+        assertTrue(handlerCalled.get(), "Handler was not called");
+
+        peer.expectDetach().respond();
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiveComplexEndodedAMQPMessageAndDecode() throws IOException {
+        final String SERIALIZED_JAVA_OBJECT_CONTENT_TYPE = "application/x-java-serialized-object";
+        final String JMS_MSG_TYPE = "x-opt-jms-msg-type";
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withDrain(false).withLinkCredit(1);
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open().addCredit(1);
+
+        peer.waitForScriptToComplete();
+
+        final AtomicReference<IncomingDelivery> received = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            received.set(delivery);
+
+            delivery.disposition(Accepted.getInstance(), true);
+        });
+
+        SimplePojo expectedContent = new SimplePojo(UUID.randomUUID());
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(expectedContent);
+        oos.flush();
+        oos.close();
+        byte[] bytes = baos.toByteArray();
+
+        peer.expectDisposition().withState().accepted().withSettled(true);
+        peer.remoteTransfer().withDeliveryTag(new byte[] {0})
+                             .withDeliveryId(0)
+                             .withProperties().withContentType(SERIALIZED_JAVA_OBJECT_CONTENT_TYPE).also()
+                             .withMessageAnnotations().withAnnotation("x-opt-jms-msg-type", (byte) 1).also()
+                             .withBody().withData(bytes).also()
+                             .now();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(received.get());
+
+        ProtonBuffer buffer = received.get().readAll();
+
+        MessageAnnotations annotations;
+        Properties properties;
+        Section<?> body;
+
+        try {
+            annotations = (MessageAnnotations) decoder.readObject(buffer, decoderState);
+            assertNotNull(annotations);
+            assertTrue(annotations.getValue().containsKey(Symbol.valueOf(JMS_MSG_TYPE)));
+        } catch (Exception ex) {
+            fail("Should not encouter error on decode of MessageAnnotations: " + ex);
+        } finally {
+            decoderState.reset();
+        }
+
+        try {
+            properties = (Properties) decoder.readObject(buffer, decoderState);
+            assertNotNull(properties);
+            assertEquals(SERIALIZED_JAVA_OBJECT_CONTENT_TYPE, properties.getContentType());
+        } catch (Exception ex) {
+            fail("Should not encouter error on decode of Properties: " + ex);
+        } finally {
+            decoderState.reset();
+        }
+
+        try {
+            body = (Section<?>) decoder.readObject(buffer, decoderState);
+            assertNotNull(body);
+            assertTrue(body instanceof Data);
+            Data payload = (Data) body;
+            assertEquals(bytes.length, payload.getBinary().getLength());
+        } catch (Exception ex) {
+            fail("Should not encouter error on decode of Body section: " + ex);
+        } finally {
+            decoderState.reset();
+        }
+
+        peer.expectClose().respond();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverCreditNotClearedUntilClosedAfterRemoteClosed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(10);
+        peer.remoteDetach().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open().addCredit(10);
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach();
+
+        assertEquals(10, receiver.getCredit());
+        receiver.close();
+        assertEquals(0, receiver.getCredit());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverCreditNotClearedUntilClosedAfterSessionRemoteClosed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(10);
+        peer.remoteEnd().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open().addCredit(10);
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach();
+
+        assertEquals(10, receiver.getCredit());
+        receiver.close();
+        assertEquals(0, receiver.getCredit());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverCreditNotClearedUntilClosedAfterConnectionRemoteClosed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(10);
+        peer.remoteClose().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open().addCredit(10);
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach();
+
+        assertEquals(10, receiver.getCredit());
+        receiver.close();
+        assertEquals(0, receiver.getCredit());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverCreditNotClearedUntilClosedAfterEngineShutdown() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(10);
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("test").open().addCredit(10);
+
+        peer.waitForScriptToComplete();
+
+        engine.shutdown();
+
+        assertEquals(10, receiver.getCredit());
+        receiver.close();
+        assertEquals(0, receiver.getCredit());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverHonorsDeliverySetEventHandlers() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0).queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.remoteDisposition().withSettled(true)
+                                .withRole(Role.SENDER.getValue())
+                                .withState().accepted()
+                                .withFirst(0).queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicInteger additionalDeliveryCounter = new AtomicInteger();
+        final AtomicInteger dispositionCounter = new AtomicInteger();
+
+        final ArrayList<IncomingDelivery> deliveries = new ArrayList<>();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+            delivery.deliveryReadHandler((target) -> {
+                additionalDeliveryCounter.incrementAndGet();
+            });
+            delivery.deliveryStateUpdatedHandler((target) -> {
+                dispositionCounter.incrementAndGet();
+                deliveries.add(delivery);
+            });
+        });
+
+        receiver.deliveryStateUpdatedHandler((delivery) -> {
+            fail("Should not have updated this handler.");
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+        receiver.close();
+
+        assertEquals(1, deliveryCounter.get(), "Should only be one initial delivery");
+        assertEquals(1, additionalDeliveryCounter.get(), "Should be a second delivery update at the delivery handler");
+        assertEquals(1, dispositionCounter.get(), "Not all deliveries received dispositions");
+
+        byte deliveryTag = 0;
+
+        for (IncomingDelivery delivery : deliveries) {
+            assertEquals(deliveryTag++, delivery.getTag().tagBuffer().getByte(0), "Delivery not updated in correct order");
+            assertTrue(delivery.isRemotelySettled(), "Delivery should be marked as remotely setted");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReceiverAbortedHandlerCalledWhenSet() throws Exception {
+        doTestReceiverReadHandlerOrAbortHandlerCalled(true);
+    }
+
+    @Test
+    public void testReceiverReadHandlerCalledForAbortWhenAbortedNotSet() throws Exception {
+        doTestReceiverReadHandlerOrAbortHandlerCalled(false);
+    }
+
+    private void doTestReceiverReadHandlerOrAbortHandlerCalled(boolean setAbortHandler) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0).queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withAborted(true)
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicInteger deliveryAbortedInReadEventCounter = new AtomicInteger();
+        final AtomicInteger deliveryAbortedCounter = new AtomicInteger();
+
+        receiver.deliveryReadHandler(delivery -> {
+            if (delivery.isAborted()) {
+                deliveryAbortedInReadEventCounter.incrementAndGet();
+            } else {
+                deliveryCounter.incrementAndGet();
+            }
+        });
+
+        if (setAbortHandler) {
+            receiver.deliveryAbortedHandler(delivery -> {
+                deliveryAbortedCounter.incrementAndGet();
+            });
+        }
+
+        receiver.deliveryStateUpdatedHandler((delivery) -> {
+            fail("Should not have updated this handler.");
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+        receiver.close();
+
+        assertEquals(1, deliveryCounter.get(), "Should only be one initial delivery");
+        if (setAbortHandler) {
+            assertEquals(0, deliveryAbortedInReadEventCounter.get(), "Should be no aborted delivery in read event");
+            assertEquals(1, deliveryAbortedCounter.get(), "Should only be one aborted delivery events");
+        } else {
+            assertEquals(1, deliveryAbortedInReadEventCounter.get(), "Should only be no aborted delivery in read event");
+            assertEquals(0, deliveryAbortedCounter.get(), "Should be no aborted delivery events");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testIncomingDeliveryReadEventSignaledWhenNoAbortedHandlerSet() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0).queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withAborted(true)
+                             .withMore(false)
+                             .withMessageFormat(0).queue();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicInteger deliveryAbortedCounter = new AtomicInteger();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+            delivery.deliveryReadHandler((target) -> {
+                if (target.isAborted()) {
+                    deliveryAbortedCounter.incrementAndGet();
+                }
+            });
+        });
+
+        receiver.deliveryStateUpdatedHandler((delivery) -> {
+            fail("Should not have updated this handler.");
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+        receiver.close();
+
+        assertEquals(1, deliveryCounter.get(), "Should only be one initial delivery");
+        assertEquals(1, deliveryAbortedCounter.get(), "Should only be one aborted delivery");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionWindowOpenedAfterDeliveryRead() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().withIncomingWindow(1).respond();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withPayload(payload)
+                             .withMessageFormat(0).queue();
+        peer.expectFlow().withLinkCredit(1).withIncomingWindow(1);
+        peer.remoteTransfer().withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withPayload(payload)
+                             .withMessageFormat(0).queue();
+        peer.expectFlow().withLinkCredit(0).withIncomingWindow(1);
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setIncomingCapacity(1024).open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+            delivery.readAll();
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+        receiver.close();
+
+        assertEquals(2, deliveryCounter.get(), "Should only be one initial delivery");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionWindowOpenedAfterDeliveryReadFromSplitFramedTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().withIncomingWindow(1).respond();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withPayload(payload)
+                             .withMessageFormat(0).queue();
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(true)
+                             .withPayload(payload)
+                             .withMessageFormat(0).queue();
+        peer.expectFlow().withLinkCredit(3).withIncomingWindow(0);
+        peer.expectFlow().withLinkCredit(3).withIncomingWindow(1);
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setIncomingCapacity(1024).open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicReference<IncomingDelivery> delivery = new AtomicReference<>();
+
+        receiver.deliveryReadHandler(incoming -> {
+            if (deliveryCounter.getAndIncrement() == 0) {
+                delivery.set(incoming);
+                delivery.get().readAll();
+            }
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+
+        assertEquals(2, deliveryCounter.get(), "Should only be one initial delivery");
+        assertTrue(delivery.get().available() > 0);
+
+        receiver.addCredit(1);
+
+        delivery.get().readAll();
+
+        receiver.close();
+
+        assertEquals(2, deliveryCounter.get(), "Should only be one initial delivery");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testIncomingDeliveryTracksTransferInCount() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(1);
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setIncomingCapacity(1024).open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicReference<IncomingDelivery> received = new AtomicReference<>();
+
+        receiver.deliveryReadHandler(delivery -> {
+            received.compareAndSet(null, delivery);
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withPayload(payload).now();
+
+        assertNotNull(received.get());
+        assertEquals(1, received.get().getTransferCount());
+
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withMore(false)
+                             .withPayload(payload).now();
+
+        assertNotNull(received.get());
+        assertEquals(2, received.get().getTransferCount());
+
+        receiver.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSettleDeliveryAfterEngineShutdown() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicReference<IncomingDelivery> receivedDelivery = new AtomicReference<>();
+        final byte[] payload = new byte[] { 1 };
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("receiver");
+        receiver.addCredit(1);
+
+        // Receiver 1 handlers for delivery processing.
+        receiver.deliveryReadHandler(delivery -> {
+            receivedDelivery.set(delivery);
+        });
+
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+
+        engine.shutdown();
+
+        try {
+            receivedDelivery.get().settle();
+            fail("Should not allow for settlement since engine was manually shut down");
+        } catch (EngineShutdownException ese) {}
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReadAllDeliveryDataWhenSessionWindowInForceAndLinkIsClosed() throws Exception {
+        testReadAllDeliveryDataWhenSessionWindowInForceButLinkCannotWrite(true, false, false, false);
+    }
+
+    @Test
+    public void testReadAllDeliveryDataWhenSessionWindowInForceAndSessionIsClosed() throws Exception {
+        testReadAllDeliveryDataWhenSessionWindowInForceButLinkCannotWrite(false, true, false, false);
+    }
+
+    @Test
+    public void testReadAllDeliveryDataWhenSessionWindowInForceAndConnectionIsClosed() throws Exception {
+        testReadAllDeliveryDataWhenSessionWindowInForceButLinkCannotWrite(false, false, true, false);
+    }
+
+    @Test
+    public void testReadAllDeliveryDataWhenSessionWindowInForceAndEngineIsShutdown() throws Exception {
+        testReadAllDeliveryDataWhenSessionWindowInForceButLinkCannotWrite(false, false, false, true);
+    }
+
+    private void testReadAllDeliveryDataWhenSessionWindowInForceButLinkCannotWrite(boolean closeLink, boolean closeSession, boolean closeConnection, boolean shutdown) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().withIncomingWindow(1).respond();
+        peer.expectAttach().withRole(Role.RECEIVER.getValue()).respond();
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withPayload(payload)
+                             .withMessageFormat(0).queue();
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withPayload(payload)
+                             .withMessageFormat(0).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setIncomingCapacity(1024).open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicReference<IncomingDelivery> delivery = new AtomicReference<>();
+
+        receiver.deliveryReadHandler(incoming -> {
+            if (deliveryCounter.getAndAdd(1) == 0) {
+                delivery.set(incoming);
+                incoming.readAll();
+            }
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+
+        peer.waitForScriptToComplete();
+
+        if (closeLink) {
+            peer.expectDetach().withClosed(true).respond();
+            receiver.close();
+        }
+        if (closeSession) {
+            peer.expectEnd().respond();
+            session.close();
+        }
+        if (closeConnection) {
+            peer.expectClose().respond();
+            connection.close();
+        }
+        if (shutdown) {
+            engine.shutdown();
+        }
+
+        assertNotNull(delivery.get());
+        assertEquals(2, deliveryCounter.get(), "Should only be one initial delivery");
+
+        delivery.get().readAll();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testWalkUnsettledAfterReceivingTransfersThatCrossSignedIntDeliveryIdRange() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] payload = new byte[] { 1 };
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond().withNextOutgoingId(Integer.MAX_VALUE);
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(Integer.MAX_VALUE)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+        peer.remoteTransfer().withDeliveryId(Integer.MAX_VALUE + 1)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("receiver");
+
+        receiver.addCredit(2);
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withFirst(Integer.MAX_VALUE)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(Integer.MAX_VALUE + 1)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(receiver.hasUnsettled());
+        assertEquals(2, receiver.unsettled().size());
+        receiver.disposition((delivery) -> true, Accepted.getInstance(), true);
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testUnsettledCollectionDispositionsAfterReceivingTransfersThatCrossSignedIntDeliveryIdRange() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] payload = new byte[] { 1 };
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond().withNextOutgoingId(Integer.MAX_VALUE);
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2);
+        peer.remoteTransfer().withDeliveryId(Integer.MAX_VALUE)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+        peer.remoteTransfer().withDeliveryId(Integer.MAX_VALUE + 1)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("receiver");
+
+        receiver.addCredit(2);
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withFirst(Integer.MAX_VALUE)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(Integer.MAX_VALUE + 1)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(receiver.hasUnsettled());
+        assertEquals(2, receiver.unsettled().size());
+        receiver.unsettled().forEach((delivery) -> {
+            delivery.disposition(Accepted.getInstance(), true);
+        });
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testWalkUnsettledAfterReceivingTransfersThatCrossUnsignedIntDeliveryIdRange() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] payload = new byte[] { 1 };
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond().withNextOutgoingId(UnsignedInteger.MAX_VALUE.intValue());
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(3);
+        peer.remoteTransfer().withDeliveryId(UnsignedInteger.MAX_VALUE.intValue())
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+        peer.remoteTransfer().withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("receiver");
+
+        receiver.addCredit(3);
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withFirst(UnsignedInteger.MAX_VALUE.intValue())
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(1)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(receiver.hasUnsettled());
+        assertEquals(3, receiver.unsettled().size());
+        receiver.disposition((delivery) -> true, Accepted.getInstance(), true);
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testUnsettledCollectionDispositionAfterReceivingTransfersThatCrossUnsignedIntDeliveryIdRange() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] payload = new byte[] { 1 };
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond().withNextOutgoingId(UnsignedInteger.MAX_VALUE.intValue());
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(3);
+        peer.remoteTransfer().withDeliveryId(UnsignedInteger.MAX_VALUE.intValue())
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+        peer.remoteTransfer().withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {2})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Receiver receiver = session.receiver("receiver");
+
+        receiver.addCredit(3);
+        receiver.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withFirst(UnsignedInteger.MAX_VALUE.intValue())
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDisposition().withFirst(1)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(receiver.hasUnsettled());
+        assertEquals(3, receiver.unsettled().size());
+        receiver.unsettled().forEach((delivery) -> {
+            delivery.disposition(Accepted.getInstance(), true);
+        });
+
+        receiver.close();
+        session.close();
+        connection.close();
+
+        // Check post conditions and done.
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testIncomingWindowRefilledWithBytesPreviouslyReadOnAbortedTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[256];
+        Arrays.fill(payload, (byte) 127);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().withIncomingWindow(2).respond();
+        peer.expectAttach().respond();
+        peer.expectFlow().withLinkCredit(2).withNextIncomingId(1);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(true)
+                             .withMessageFormat(0)
+                             .withPayload(payload).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+        session.setIncomingCapacity((int) (connection.getMaxFrameSize() * 2));
+        session.open();
+        Receiver receiver = session.receiver("test");
+
+        final AtomicInteger deliveryCounter = new AtomicInteger();
+        final AtomicInteger deliveryAbortedCounter = new AtomicInteger();
+
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryCounter.incrementAndGet();
+            if (delivery.isAborted()) {
+                deliveryAbortedCounter.incrementAndGet();
+            }
+        });
+
+        receiver.deliveryStateUpdatedHandler((delivery) -> {
+            fail("Should not have updated this handler.");
+        });
+
+        receiver.open();
+        receiver.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectFlow().withLinkCredit(1).withIncomingWindow(2).withNextIncomingId(3);
+        peer.expectDetach().respond();
+
+        assertEquals((connection.getMaxFrameSize() * 2) - payload.length, session.getRemainingIncomingCapacity());
+
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withAborted(true)
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withPayload(payload).now();
+
+        assertEquals(connection.getMaxFrameSize() * 2, session.getRemainingIncomingCapacity());
+
+        receiver.close();
+
+        assertEquals(2, deliveryCounter.get(), "Should have received two delivery read events");
+        assertEquals(1, deliveryAbortedCounter.get(), "Should only be one aborted delivery event");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSenderTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSenderTest.java
new file mode 100644
index 0000000..3f970c0
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSenderTest.java
@@ -0,0 +1,4359 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.AcceptedMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.ModifiedMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.RejectedMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.messaging.ReleasedMatcher;
+import org.apache.qpid.protonj2.test.driver.matchers.transactions.TransactionalStateMatcher;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.ReceiverSettleMode;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SenderSettleMode;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test the {@link ProtonSender}
+ */
+@Timeout(20)
+public class ProtonSenderTest extends ProtonEngineTestSupport {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonSenderTest.class);
+
+    @Test
+    public void testLocalLinkStateCannotBeChangedAfterOpen() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+
+        sender.setProperties(new HashMap<>());
+
+        sender.open();
+
+        try {
+            sender.setProperties(new HashMap<>());
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            sender.setDesiredCapabilities(new Symbol[] { AmqpError.DECODE_ERROR });
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            sender.setOfferedCapabilities(new Symbol[] { AmqpError.DECODE_ERROR });
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            sender.setSenderSettleMode(SenderSettleMode.MIXED);
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            sender.setSource(new Source());
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            sender.setTarget(new Target());
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        try {
+            sender.setMaxMessageSize(UnsignedLong.ZERO);
+            fail("Cannot alter local link initial state data after sender opened.");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        sender.detach();
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderEmitsOpenAndCloseEvents() throws Exception {
+        doTestSenderEmitsEvents(false);
+    }
+
+    @Test
+    public void testSenderEmitsOpenAndDetachEvents() throws Exception {
+        doTestSenderEmitsEvents(true);
+    }
+
+    private void doTestSenderEmitsEvents(boolean detach) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean senderLocalOpen = new AtomicBoolean();
+        final AtomicBoolean senderLocalClose = new AtomicBoolean();
+        final AtomicBoolean senderLocalDetach = new AtomicBoolean();
+        final AtomicBoolean senderRemoteOpen = new AtomicBoolean();
+        final AtomicBoolean senderRemoteClose = new AtomicBoolean();
+        final AtomicBoolean senderRemoteDetach = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.localOpenHandler(result -> senderLocalOpen.set(true))
+              .localCloseHandler(result -> senderLocalClose.set(true))
+              .localDetachHandler(result -> senderLocalDetach.set(true))
+              .openHandler(result -> senderRemoteOpen.set(true))
+              .detachHandler(result -> senderRemoteDetach.set(true))
+              .closeHandler(result -> senderRemoteClose.set(true));
+
+        sender.open();
+
+        assertNull(sender.getDeliveryTagGenerator());
+
+        if (detach) {
+            sender.detach();
+        } else {
+            sender.close();
+        }
+
+        assertTrue(senderLocalOpen.get(), "Sender should have reported local open");
+        assertTrue(senderRemoteOpen.get(), "Sender should have reported remote open");
+
+        if (detach) {
+            assertFalse(senderLocalClose.get(), "Sender should not have reported local close");
+            assertTrue(senderLocalDetach.get(), "Sender should have reported local detach");
+            assertFalse(senderRemoteClose.get(), "Sender should not have reported remote close");
+            assertTrue(senderRemoteDetach.get(), "Sender should have reported remote close");
+        } else {
+            assertTrue(senderLocalClose.get(), "Sender should have reported local close");
+            assertFalse(senderLocalDetach.get(), "Sender should not have reported local detach");
+            assertTrue(senderRemoteClose.get(), "Sender should have reported remote close");
+            assertFalse(senderRemoteDetach.get(), "Sender should not have reported remote close");
+        }
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderRoutesDetachEventToCloseHandlerIfNonSset() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean senderLocalOpen = new AtomicBoolean();
+        final AtomicBoolean senderLocalClose = new AtomicBoolean();
+        final AtomicBoolean senderRemoteOpen = new AtomicBoolean();
+        final AtomicBoolean senderRemoteClose = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.localOpenHandler(result -> senderLocalOpen.set(true))
+              .localCloseHandler(result -> senderLocalClose.set(true))
+              .openHandler(result -> senderRemoteOpen.set(true))
+              .closeHandler(result -> senderRemoteClose.set(true));
+
+        sender.open();
+        sender.detach();
+
+        assertTrue(senderLocalOpen.get(), "Sender should have reported local open");
+        assertTrue(senderRemoteOpen.get(), "Sender should have reported remote open");
+        assertTrue(senderLocalClose.get(), "Sender should have reported local detach");
+        assertTrue(senderRemoteClose.get(), "Sender should have reported remote detach");
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderEnforcesOneActiveDeliveryAtNextAPI() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        assertNotNull(sender.next());
+
+        assertThrows(IllegalStateException.class, () -> sender.next());
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderReceivesParentSessionClosedEvent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean parentClosed = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.parentEndpointClosedHandler(result -> parentClosed.set(true));
+
+        sender.open();
+
+        session.close();
+
+        assertTrue(parentClosed.get(), "Sender should have reported parent session closed");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderReceivesParentConnectionClosedEvent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean parentClosed = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.parentEndpointClosedHandler(result -> parentClosed.set(true));
+
+        sender.open();
+
+        connection.close();
+
+        assertTrue(parentClosed.get(), "Sender should have reported parent connection closed");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineShutdownEventNeitherEndClosed() throws Exception {
+        doTestEngineShutdownEvent(false, false);
+    }
+
+    @Test
+    public void testEngineShutdownEventLocallyClosed() throws Exception {
+        doTestEngineShutdownEvent(true, false);
+    }
+
+    @Test
+    public void testEngineShutdownEventRemotelyClosed() throws Exception {
+        doTestEngineShutdownEvent(false, true);
+    }
+
+    @Test
+    public void testEngineShutdownEventBothEndsClosed() throws Exception {
+        doTestEngineShutdownEvent(true, true);
+    }
+
+    private void doTestEngineShutdownEvent(boolean locallyClosed, boolean remotelyClosed) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean engineShutdown = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.open();
+        sender.engineShutdownHandler(result -> engineShutdown.set(true));
+
+        if (locallyClosed) {
+            if (remotelyClosed) {
+                peer.expectDetach().respond();
+            } else {
+                peer.expectDetach();
+            }
+
+            sender.close();
+        }
+
+        if (remotelyClosed && !locallyClosed) {
+            peer.remoteDetach();
+        }
+
+        engine.shutdown();
+
+        if (locallyClosed && remotelyClosed) {
+            assertFalse(engineShutdown.get(), "Should not have reported engine shutdown");
+        } else {
+            assertTrue(engineShutdown.get(), "Should have reported engine shutdown");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderOpenWithNoSenderOrReceiverSettleModes() throws Exception {
+        doTestOpenSenderWithConfiguredSenderAndReceiverSettlementModes(null, null);
+    }
+
+    @Test
+    public void testSenderOpenWithSettledAndFirst() throws Exception {
+        doTestOpenSenderWithConfiguredSenderAndReceiverSettlementModes(SenderSettleMode.SETTLED, ReceiverSettleMode.FIRST);
+    }
+
+    @Test
+    public void testSenderOpenWithUnsettledAndSecond() throws Exception {
+        doTestOpenSenderWithConfiguredSenderAndReceiverSettlementModes(SenderSettleMode.UNSETTLED, ReceiverSettleMode.SECOND);
+    }
+
+    private void doTestOpenSenderWithConfiguredSenderAndReceiverSettlementModes(SenderSettleMode senderMode, ReceiverSettleMode receiverMode) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withSndSettleMode(senderMode == null ? null : senderMode.byteValue())
+                           .withRcvSettleMode(receiverMode == null ? null : receiverMode.byteValue())
+                           .respond()
+                           .withSndSettleMode(senderMode == null ? null : senderMode.byteValue())
+                           .withRcvSettleMode(receiverMode == null ? null : receiverMode.byteValue());
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender");
+        sender.setSenderSettleMode(senderMode);
+        sender.setReceiverSettleMode(receiverMode);
+        sender.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().respond();
+
+        if (senderMode != null) {
+            assertEquals(senderMode, sender.getSenderSettleMode());
+        } else {
+            assertEquals(SenderSettleMode.MIXED, sender.getSenderSettleMode());
+        }
+        if (receiverMode != null) {
+            assertEquals(receiverMode, sender.getReceiverSettleMode());
+        } else {
+            assertEquals(ReceiverSettleMode.FIRST, sender.getReceiverSettleMode());
+        }
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderOpenAndCloseAreIdempotent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.open();
+
+        // Should not emit another attach frame
+        sender.open();
+
+        sender.close();
+
+        // Should not emit another detach frame
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCreateSenderAndClose() throws Exception {
+        doTestCreateSenderAndCloseOrDetachLink(true);
+    }
+
+    @Test
+    public void testCreateSenderAndDetach() throws Exception {
+        doTestCreateSenderAndCloseOrDetachLink(false);
+    }
+
+    private void doTestCreateSenderAndCloseOrDetachLink(boolean close) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.expectDetach().withClosed(close).respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.open();
+
+        assertTrue(sender.isSender());
+        assertFalse(sender.isReceiver());
+
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineEmitsAttachAfterLocalSenderOpened() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.open();
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenBeginAttachBeforeRemoteResponds() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.expectBegin();
+        peer.expectAttach();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderFireOpenedEventAfterRemoteAttachArrives() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.openHandler(result -> {
+            senderRemotelyOpened.set(true);
+        });
+        sender.open();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderFireOpenedEventAfterRemoteAttachArrivesWithNullTarget() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond().withNullTarget();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.setSource(new Source());
+        sender.setTarget(new Target());
+        sender.openHandler(result -> {
+            senderRemotelyOpened.set(true);
+        });
+        sender.open();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+        assertNull(sender.getRemoteTarget());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenAndCloseMultipleSenders() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).respond();
+        peer.expectAttach().withHandle(1).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender1 = session.sender("sender-1");
+        sender1.open();
+        Sender sender2 = session.sender("sender-2");
+        sender2.open();
+
+        // Close in reverse order
+        sender2.close();
+        sender1.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderFireClosedEventAfterRemoteDetachArrives() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean senderRemotelyClosed = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.openHandler(result -> {
+            senderRemotelyOpened.set(true);
+        });
+        sender.closeHandler(result -> {
+            senderRemotelyClosed.set(true);
+        });
+        sender.open();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+
+        sender.close();
+
+        assertTrue(senderRemotelyClosed.get(), "Sender remote closed event did not fire");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderFireClosedEventAfterRemoteDetachArrivesBeforeLocalClose() throws Exception {
+        doTestSenderFireEventAfterRemoteDetachArrivesBeforeLocalClose(true);
+    }
+
+    @Test
+    public void testSenderFireDetachEventAfterRemoteDetachArrivesBeforeLocalClose() throws Exception {
+        doTestSenderFireEventAfterRemoteDetachArrivesBeforeLocalClose(false);
+    }
+
+    private void doTestSenderFireEventAfterRemoteDetachArrivesBeforeLocalClose(boolean close) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteDetach().withClosed(close).queue();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean senderRemotelyClosed = new AtomicBoolean();
+        final AtomicBoolean senderRemotelyDetached = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("test");
+        sender.openHandler(result -> senderRemotelyOpened.set(true));
+        sender.closeHandler(result -> senderRemotelyClosed.set(true));
+        sender.detachHandler(result -> senderRemotelyDetached.set(true));
+        sender.open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+        if (close) {
+            assertTrue(senderRemotelyClosed.get(), "Sender remote closed event did not fire");
+            assertFalse(senderRemotelyDetached.get(), "Sender remote detached event fired");
+        } else {
+            assertFalse(senderRemotelyClosed.get(), "Sender remote closed event fired");
+            assertTrue(senderRemotelyDetached.get(), "Sender remote closed event did not fire");
+        }
+
+        peer.expectDetach().withClosed(close);
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testRemotelyCloseSenderAndOpenNewSenderImmediatelyAfterWithNewLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(true, false);
+    }
+
+    @Test
+    public void testRemotelyDetachSenderAndOpenNewSenderImmediatelyAfterWithNewLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(false, false);
+    }
+
+    @Test
+    public void testRemotelyCloseSenderAndOpenNewSenderImmediatelyAfterWithSameLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(true, true);
+    }
+
+    @Test
+    public void testRemotelyDetachSenderAndOpenNewSenderImmediatelyAfterWithSameLinkName() throws Exception {
+        doTestRemotelyTerminateLinkAndThenCreateNewLink(false, true);
+    }
+
+    private void doTestRemotelyTerminateLinkAndThenCreateNewLink(boolean close, boolean sameLinkName) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        String firstLinkName = "test-link-1";
+        String secondLinkName = sameLinkName ? firstLinkName : "test-link-2";
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withRole(Role.SENDER.getValue()).respond();
+        peer.remoteDetach().withClosed(close).queue();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean senderRemotelyClosed = new AtomicBoolean();
+        final AtomicBoolean senderRemotelyDetached = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender(firstLinkName);
+        sender.openHandler(result -> senderRemotelyOpened.set(true));
+        sender.closeHandler(result -> senderRemotelyClosed.set(true));
+        sender.detachHandler(result -> senderRemotelyDetached.set(true));
+        sender.open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+        if (close) {
+            assertTrue(senderRemotelyClosed.get(), "Sender remote closed event did not fire");
+            assertFalse(senderRemotelyDetached.get(), "Sender remote detached event fired");
+        } else {
+            assertFalse(senderRemotelyClosed.get(), "Sender remote closed event fired");
+            assertTrue(senderRemotelyDetached.get(), "Sender remote closed event did not fire");
+        }
+
+        peer.expectDetach().withClosed(close);
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        peer.waitForScriptToComplete();
+        peer.expectAttach().withHandle(0).withRole(Role.SENDER.getValue()).respond();
+        peer.expectDetach().withClosed(close).respond();
+
+        // Reset trackers
+        senderRemotelyOpened.set(false);
+        senderRemotelyClosed.set(false);
+        senderRemotelyDetached.set(false);
+
+        sender = session.sender(secondLinkName);
+        sender.openHandler(result -> senderRemotelyOpened.set(true));
+        sender.closeHandler(result -> senderRemotelyClosed.set(true));
+        sender.detachHandler(result -> senderRemotelyDetached.set(true));
+        sender.open();
+
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+        if (close) {
+            assertTrue(senderRemotelyClosed.get(), "Sender remote closed event did not fire");
+            assertFalse(senderRemotelyDetached.get(), "Sender remote detached event fired");
+        } else {
+            assertFalse(senderRemotelyClosed.get(), "Sender remote closed event fired");
+            assertTrue(senderRemotelyDetached.get(), "Sender remote closed event did not fire");
+        }
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testConnectionSignalsRemoteSenderOpen() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("receiver")
+                           .withHandle(0)
+                           .withRole(Role.RECEIVER.getValue())
+                           .withInitialDeliveryCount(0)
+                           .onChannel(0).queue();
+        peer.expectAttach();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+        final AtomicReference<Sender> sender = new AtomicReference<>();
+
+        Connection connection = engine.start();
+
+        connection.senderOpenHandler(result -> {
+            senderRemotelyOpened.set(true);
+            sender.set(result);
+        });
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+
+        sender.get().open();
+        sender.get().close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotOpenSenderAfterSessionClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+
+        session.close();
+
+        try {
+            sender.open();
+            fail("Should not be able to open a link from a closed session.");
+        } catch (IllegalStateException ise) {}
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotOpenSenderAfterSessionRemotelyClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteEnd().queue();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        Sender sender = session.sender("test");
+        session.open();
+
+        try {
+            sender.open();
+            fail("Should not be able to open a link from a remotely closed session.");
+        } catch (IllegalStateException ise) {}
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testGetCurrentDeliveryFromSender() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).respond();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+
+        sender.open();
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        assertFalse(delivery.isAborted());
+        assertTrue(delivery.isPartial());
+        assertFalse(delivery.isSettled());
+        assertFalse(delivery.isRemotelySettled());
+
+        // Always return same delivery until completed.
+        assertSame(delivery, sender.current());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderGetsCreditOnIncomingFlow() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+
+        assertFalse(sender.isSendable());
+
+        sender.open();
+
+        assertEquals(10, sender.getCredit());
+        assertTrue(sender.isSendable());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSendSmallPayloadWhenCreditAvailable() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte [] payloadBuffer = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payloadBuffer);
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(payloadBuffer);
+
+        Sender sender = session.sender("sender-1");
+
+        assertFalse(sender.isSendable());
+
+        sender.creditStateUpdateHandler(handler -> {
+            if (handler.isSendable()) {
+                handler.next().setTag(new byte[] {0}).writeBytes(payload);
+            }
+        });
+
+        sender.open();
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSendTramsferWithNonDefaultMessageFormat() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte [] payloadBuffer = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withMessageFormat(17).withPayload(payloadBuffer);
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(payloadBuffer);
+
+        Sender sender = session.sender("sender-1");
+
+        assertFalse(sender.isSendable());
+
+        sender.creditStateUpdateHandler(handler -> {
+            if (handler.isSendable()) {
+                handler.next().setTag(new byte[] {0}).setMessageFormat(17).writeBytes(payload);
+            }
+        });
+
+        sender.open();
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderSignalsDeliveryUpdatedOnSettledThenSettleFromLinkAPI() throws Exception {
+        doTestSenderSignalsDeliveryUpdatedOnSettled(true);
+    }
+
+    @Test
+    public void testSenderSignalsDeliveryUpdatedOnSettledThenSettleDelivery() throws Exception {
+        doTestSenderSignalsDeliveryUpdatedOnSettled(false);
+    }
+
+    private void doTestSenderSignalsDeliveryUpdatedOnSettled(boolean settleFromLink) {
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.remoteDisposition().withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted()
+                                .withFirst(0).queue();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+
+        final AtomicBoolean deliveryUpdatedAndSettled = new AtomicBoolean();
+        final AtomicReference<OutgoingDelivery> updatedDelivery = new AtomicReference<>();
+        sender.deliveryStateUpdatedHandler(delivery -> {
+            if (delivery.isRemotelySettled()) {
+                deliveryUpdatedAndSettled.set(true);
+            }
+
+            updatedDelivery.set(delivery);
+        });
+
+        assertFalse(sender.isSendable());
+
+        sender.creditStateUpdateHandler(handler -> {
+            if (handler.isSendable()) {
+                handler.next().setTag(new byte[] {0}).writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+            }
+        });
+
+        sender.open();
+
+        assertTrue(deliveryUpdatedAndSettled.get(), "Delivery should have been updated and state settled");
+        assertEquals(Accepted.getInstance(), updatedDelivery.get().getRemoteState());
+        assertTrue(sender.hasUnsettled());
+        assertFalse(sender.unsettled().isEmpty());
+
+        if (settleFromLink) {
+            sender.settle(delivery -> true);
+        } else {
+            updatedDelivery.get().settle();
+        }
+
+        assertFalse(sender.hasUnsettled());
+        assertTrue(sender.unsettled().isEmpty());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenSenderBeforeOpenConnection() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Create the connection but don't open, then open a session and a sender and
+        // the session begin and sender attach shouldn't go out until the connection
+        // is opened locally.
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("sender");
+        sender.open();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("sender").withRole(Role.SENDER.getValue()).respond();
+
+        // Now open the connection, expect the Open, Begin, and Attach frames
+        connection.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenSenderBeforeOpenSession() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+
+        // Create the connection and open it, then create a session and a sender
+        // and observe that the sender doesn't send its attach until the session
+        // is opened.
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        Sender sender = session.sender("sender");
+        sender.open();
+
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("sender").withRole(Role.SENDER.getValue()).respond();
+
+        // Now open the session, expect the Begin, and Attach frames
+        session.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderDetachAfterEndSent() {
+        doTestSenderClosedOrDetachedAfterEndSent(false);
+    }
+
+    @Test
+    public void testSenderCloseAfterEndSent() {
+        doTestSenderClosedOrDetachedAfterEndSent(true);
+    }
+
+    public void doTestSenderClosedOrDetachedAfterEndSent(boolean close) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("sender").withRole(Role.SENDER.getValue()).respond();
+        peer.expectEnd().respond();
+
+        // Create the connection and open it, then create a session and a sender
+        // and observe that the sender doesn't send its detach if the session has
+        // already been closed.
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("sender");
+        sender.open();
+
+        // Causes the End frame to be sent
+        session.close();
+
+        // The sender should not emit an end as the session was closed which implicitly
+        // detached the link.
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderDetachAfterCloseSent() {
+        doTestSenderClosedOrDetachedAfterCloseSent(false);
+    }
+
+    @Test
+    public void testSenderCloseAfterCloseSent() {
+        doTestSenderClosedOrDetachedAfterCloseSent(true);
+    }
+
+    public void doTestSenderClosedOrDetachedAfterCloseSent(boolean close) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).withName("sender").withRole(Role.SENDER.getValue()).respond();
+        peer.expectClose().respond();
+
+        // Create the connection and open it, then create a session and a sender
+        // and observe that the sender doesn't send its detach if the connection has
+        // already been closed.
+        Connection connection = engine.start();
+        connection.open();
+        Session session = connection.session();
+        session.open();
+        Sender sender = session.sender("sender");
+        sender.open();
+
+        // Cause an Close frame to be sent
+        connection.close();
+
+        // The sender should not emit an detach as the connection was closed which implicitly
+        // detached the link.
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNoDispositionSentAfterDeliverySettledForSender() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0});
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withState().accepted();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        Sender sender = session.sender("sender-1");
+
+        final AtomicBoolean deliverySentAfterSenable = new AtomicBoolean();
+        final AtomicReference<OutgoingDelivery> sent = new AtomicReference<>();
+
+        sender.creditStateUpdateHandler(handler -> {
+            if (handler.isSendable()) {
+                sent.set(handler.next().setTag(new byte[] {0}).writeBytes(payload));
+                deliverySentAfterSenable.set(true);
+            }
+        });
+
+        sender.open();
+
+        assertTrue(deliverySentAfterSenable.get(), "Delivery should have been sent after credit arrived");
+
+        assertNull(sender.current());
+
+        sent.get().disposition(Accepted.getInstance(), true);
+
+        OutgoingDelivery delivery2 = sender.next();
+        assertNotSame(delivery2, sent.get());
+        delivery2.disposition(Released.getInstance(), true);
+
+        assertFalse(sender.hasUnsettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderCannotSendAfterConnectionClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+
+        assertFalse(sender.isSendable());
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        sender.open();
+
+        assertEquals(10, sender.getCredit());
+        assertTrue(sender.isSendable());
+
+        connection.close();
+
+        assertFalse(sender.isSendable());
+        try {
+            delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 1 }));
+            fail("Should not be able to write to delivery after connection closed.");
+        } catch (IllegalStateException ise) {
+            // Should not allow writes on past delivery instances after connection closed
+        }
+
+        try {
+            sender.next();
+            fail("Should not be able get next after connection closed");
+        } catch (IllegalStateException ise) {
+            // Should not allow next message after close of connection
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderCannotSendAfterSessionClosed() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+
+        assertFalse(sender.isSendable());
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        sender.open();
+
+        assertEquals(10, sender.getCredit());
+        assertTrue(sender.isSendable());
+
+        session.close();
+
+        assertFalse(sender.isSendable());
+        try {
+            delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 1 }));
+            fail("Should not be able to write to delivery after session closed.");
+        } catch (IllegalStateException ise) {
+            // Should not allow writes on past delivery instances after session closed
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderWriteBytesThrowsEngineFailedAfterConnectionDropped() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.dropAfterLastHandler();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+        OutgoingDelivery delivery = sender.next();
+
+        assertNotNull(delivery);
+        assertTrue(sender.isSendable());
+
+        try {
+            delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 1 }));
+            fail("Should not be able to write to delivery afters simulated connection drop.");
+        } catch (EngineFailedException efe) {
+            // Should not allow writes on past delivery instances after connection dropped
+            assertTrue(efe.getCause() instanceof UncheckedIOException);
+            LOG.debug("Caught expected IO exception from write to broken connection", efe);
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testSendMultiFrameDeliveryAndSingleFrameDeliveryOnSingleSessionFromDifferentSenders() {
+        doMultiplexMultiFrameDeliveryOnSingleSessionOutgoingTestImpl(false);
+    }
+
+    @Test
+    public void testMultipleMultiFrameDeliveriesOnSingleSessionFromDifferentSenders() {
+        doMultiplexMultiFrameDeliveryOnSingleSessionOutgoingTestImpl(true);
+    }
+
+    private void doMultiplexMultiFrameDeliveryOnSingleSessionOutgoingTestImpl(boolean bothDeliveriesMultiFrame) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        int contentLength1 = 6000;
+        int frameSizeLimit = 4000;
+        int contentLength2 = 2000;
+        if (bothDeliveriesMultiFrame) {
+            contentLength2 = 6000;
+        }
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(frameSizeLimit).respond().withContainerId("driver").withMaxFrameSize(frameSizeLimit);
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+
+        Connection connection = engine.start();
+        connection.setMaxFrameSize(frameSizeLimit);
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        String linkName1 = "Sender1";
+        Sender sender1 = session.sender(linkName1);
+        sender1.open();
+
+        String linkName2 = "Sender2";
+        Sender sender2 = session.sender(linkName2);
+        sender2.open();
+
+        final AtomicBoolean sender1MarkedSendable = new AtomicBoolean();
+        sender1.creditStateUpdateHandler(handler -> {
+            sender1MarkedSendable.set(handler.isSendable());
+        });
+
+        final AtomicBoolean sender2MarkedSendable = new AtomicBoolean();
+        sender2.creditStateUpdateHandler(handler -> {
+            sender2MarkedSendable.set(handler.isSendable());
+        });
+
+        peer.remoteFlow().withHandle(0)
+                         .withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).now();
+        peer.remoteFlow().withHandle(1)
+                         .withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).now();
+
+        assertTrue(sender1MarkedSendable.get(), "Sender 1 should now be sendable");
+        assertTrue(sender2MarkedSendable.get(), "Sender 2 should now be sendable");
+
+        // Frames are not multiplexed for large deliveries as we write the full
+        // writable portion out when a write is called.
+
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(true)
+                             .withState().accepted()
+                             .withDeliveryId(0)
+                             .withMore(true)
+                             .withDeliveryTag(new byte[] {1});
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(true)
+                             .withState().accepted()
+                             .withDeliveryId(0)
+                             .withMore(false)
+                             .withDeliveryTag(nullValue());
+        peer.expectTransfer().withHandle(1)
+                             .withSettled(true)
+                             .withState().accepted()
+                             .withDeliveryId(1)
+                             .withMore(bothDeliveriesMultiFrame)
+                             .withDeliveryTag(new byte[] {2});
+        if (bothDeliveriesMultiFrame) {
+            peer.expectTransfer().withHandle(1)
+                                 .withSettled(true)
+                                 .withState().accepted()
+                                 .withDeliveryId(1)
+                                 .withMore(false)
+                                 .withDeliveryTag(nullValue());
+        }
+
+        ProtonBuffer messageContent1 = createContentBuffer(contentLength1);
+        OutgoingDelivery delivery1 = sender1.next();
+        delivery1.setTag(new byte[] { 1 });
+        delivery1.disposition(Accepted.getInstance(), true);
+        delivery1.writeBytes(messageContent1);
+
+        ProtonBuffer messageContent2 = createContentBuffer(contentLength2);
+        OutgoingDelivery delivery2 = sender2.next();
+        delivery2.setTag(new byte[] { 2 });
+        delivery2.disposition(Accepted.getInstance(), true);
+        delivery2.writeBytes(messageContent2);
+
+        peer.expectClose().respond();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testMaxFrameSizeOfPeerHasEffect() {
+        doMaxFrameSizeTestImpl(0, 0, 5700, 1);
+        doMaxFrameSizeTestImpl(1024, 0, 5700, 6);
+    }
+
+    @Test
+    public void testMaxFrameSizeOutgoingFrameSizeLimitHasEffect() {
+        doMaxFrameSizeTestImpl(0, 512, 5700, 12);
+        doMaxFrameSizeTestImpl(1024, 512, 5700, 12);
+        doMaxFrameSizeTestImpl(1024, 2048, 5700, 6);
+    }
+
+    void doMaxFrameSizeTestImpl(int remoteMaxFrameSize, int outboundFrameSizeLimit, int contentLength, int expectedNumFrames) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        if (outboundFrameSizeLimit == 0) {
+            if (remoteMaxFrameSize == 0) {
+                peer.expectOpen().respond();
+            } else {
+                peer.expectOpen().respond().withMaxFrameSize(remoteMaxFrameSize);
+            }
+        } else {
+            if (remoteMaxFrameSize == 0) {
+                peer.expectOpen().withMaxFrameSize(outboundFrameSizeLimit).respond();
+            } else {
+                peer.expectOpen().withMaxFrameSize(outboundFrameSizeLimit)
+                                 .respond()
+                                 .withMaxFrameSize(remoteMaxFrameSize);
+            }
+        }
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+
+        Connection connection = engine.start();
+        if (outboundFrameSizeLimit != 0) {
+            connection.setMaxFrameSize(outboundFrameSizeLimit);
+        }
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        String linkName = "mySender";
+        Sender sender = session.sender(linkName);
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(handler.isSendable());
+        });
+
+        peer.remoteFlow().withHandle(0)
+                         .withDeliveryCount(0)
+                         .withLinkCredit(50)
+                         .withIncomingWindow(65535)
+                         .withOutgoingWindow(65535)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).now();
+
+        assertTrue(senderMarkedSendable.get(), "Sender should now be sendable");
+
+        // This calculation isn't entirely precise, there is some added performative/frame overhead not
+        // accounted for...but values are chosen to work, and verified here.
+        final int frameCount;
+        if (remoteMaxFrameSize == 0 && outboundFrameSizeLimit == 0) {
+            frameCount = 1;
+        } else if(remoteMaxFrameSize == 0 && outboundFrameSizeLimit != 0) {
+            frameCount = (int) Math.ceil((double)contentLength / (double) outboundFrameSizeLimit);
+        } else {
+            int effectiveMaxFrameSize;
+            if (outboundFrameSizeLimit != 0) {
+                effectiveMaxFrameSize = Math.min(outboundFrameSizeLimit, remoteMaxFrameSize);
+            } else {
+                effectiveMaxFrameSize = remoteMaxFrameSize;
+            }
+
+            frameCount = (int) Math.ceil((double)contentLength / (double) effectiveMaxFrameSize);
+        }
+
+        assertEquals(expectedNumFrames, frameCount, "Unexpected number of frames calculated");
+
+        for (int i = 1; i <= expectedNumFrames; ++i) {
+            peer.expectTransfer().withHandle(0)
+                                 .withSettled(true)
+                                 .withState().accepted()
+                                 .withDeliveryId(0)
+                                 .withMore(i != expectedNumFrames ? true : false)
+                                 .withDeliveryTag(i == 1 ? notNullValue() : nullValue())
+                                 .withNonNullPayload();
+        }
+
+        ProtonBuffer messageContent = createContentBuffer(contentLength);
+        OutgoingDelivery delivery = sender.next();
+        delivery.setTag(new byte[] { 1 });
+        delivery.disposition(Accepted.getInstance(), true);
+        delivery.writeBytes(messageContent);
+
+        peer.expectClose().respond();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCompleteInProgressDeliveryWithFinalEmptyTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withAborted(anyOf(nullValue(), is(false)))
+                             .withSettled(false)
+                             .withMore(anyOf(nullValue(), is(false)))
+                             .withNullPayload();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), false);
+        delivery.streamBytes(null, true);
+
+        assertFalse(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertFalse(delivery.isSettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testAbortInProgressDelivery() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withAborted(true)
+                             .withSettled(true)
+                             .withMore(anyOf(nullValue(), is(false)))
+                             .withNullPayload();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        delivery.abort();
+
+        assertTrue(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertTrue(delivery.isSettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testAbortAlreadyAbortedDelivery() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withAborted(true)
+                             .withSettled(true)
+                             .withMore(anyOf(nullValue(), is(false)))
+                             .withNullPayload();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        assertTrue(sender.hasUnsettled());
+
+        delivery.abort();
+
+        assertTrue(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertTrue(delivery.isSettled());
+
+        // Second abort attempt should not error out or trigger additional frames
+        delivery.abort();
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testAbortOnDeliveryThatHasNoWritesIsNoOp() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.abort();
+
+        assertNull(sender.current());
+        assertTrue(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertTrue(delivery.isSettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testAbortOnDeliveryThatHasNoWritesIsNoOpThenSendUsingCurrent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.abort();
+
+        assertNull(sender.current());
+        assertTrue(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertTrue(delivery.isSettled());
+
+        try {
+            sender.next();
+        } catch (IllegalStateException ise) {
+            fail("Should not be able to next as current was not aborted since nothing was ever written.");
+        }
+
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {1})
+                             .withPayload(payload);
+        peer.expectDisposition().withFirst(0).withSettled(true).withState().accepted();
+        peer.expectDetach().withHandle(0).respond();
+
+        delivery = sender.current();
+        delivery.setTag(new byte[] {1}).writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        delivery.disposition(Accepted.getInstance(), true);
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSettleTransferWithNullDisposition() throws Exception {
+        doTestSettleTransferWithSpecifiedOutcome(null, nullValue(), true);
+    }
+
+    @Test
+    public void testSettleTransferWithAcceptedDisposition() throws Exception {
+        DeliveryState state = Accepted.getInstance();
+        AcceptedMatcher matcher = new AcceptedMatcher();
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, true);
+    }
+
+    @Test
+    public void testUnsttledDispositionOfTransferWithAcceptedOutcome() throws Exception {
+        DeliveryState state = Accepted.getInstance();
+        AcceptedMatcher matcher = new AcceptedMatcher();
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, false);
+    }
+
+    @Test
+    public void testSettleTransferWithReleasedDisposition() throws Exception {
+        DeliveryState state = Released.getInstance();
+        ReleasedMatcher matcher = new ReleasedMatcher();
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, true);
+    }
+
+    @Test
+    public void testSettleTransferWithRejectedDisposition() throws Exception {
+        DeliveryState state = new Rejected();
+        RejectedMatcher matcher = new RejectedMatcher();
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, true);
+    }
+
+    @Test
+    public void testSettleTransferWithRejectedWithErrorDisposition() throws Exception {
+        DeliveryState state = new Rejected().setError(new ErrorCondition(AmqpError.DECODE_ERROR, "test"));
+        RejectedMatcher matcher = new RejectedMatcher().withError(AmqpError.DECODE_ERROR.toString(), "test");
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, true);
+    }
+
+    @Test
+    public void testSettleTransferWithModifiedDisposition() throws Exception {
+        DeliveryState state = new Modified().setDeliveryFailed(true).setUndeliverableHere(true);
+        ModifiedMatcher matcher = new ModifiedMatcher().withDeliveryFailed(true).withUndeliverableHere(true);
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, true);
+    }
+
+    @Test
+    public void testSettleTransferWithTransactionalDisposition() throws Exception {
+        DeliveryState state = new TransactionalState().setTxnId(new Binary(new byte[] {1})).setOutcome(Accepted.getInstance());
+        TransactionalStateMatcher matcher =
+            new TransactionalStateMatcher().withTxnId(new byte[] {1}).withOutcome(new AcceptedMatcher());
+        doTestSettleTransferWithSpecifiedOutcome(state, matcher, true);
+    }
+
+    private void doTestSettleTransferWithSpecifiedOutcome(DeliveryState state, Matcher<?> stateMatcher, boolean settled) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0});
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(settled)
+                                .withState(stateMatcher);
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        Sender sender = session.sender("sender-1");
+
+        final AtomicBoolean deliverySentAfterSenable = new AtomicBoolean();
+        final AtomicReference<OutgoingDelivery> sentDelivery = new AtomicReference<>();
+        sender.creditStateUpdateHandler(handler -> {
+            sentDelivery.set(handler.next().setTag(new byte[] {0}).writeBytes(payload));
+            deliverySentAfterSenable.set(sender.isSendable());
+        });
+
+        sender.open();
+
+        assertTrue(deliverySentAfterSenable.get(), "Delivery should have been sent after credit arrived");
+
+        OutgoingDelivery delivery = sender.current();
+        assertNull(delivery);
+        sentDelivery.get().disposition(state, settled);
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testAttemptedSecondDispostionOnAlreadySettledDeliveryNull() throws Exception {
+        doTestAttemptedSecondDispostionOnAlreadySettledDelivery(Accepted.getInstance(), null);
+    }
+
+    @Test
+    public void testAttemptedSecondDispostionOnAlreadySettledDeliveryReleased() throws Exception {
+        doTestAttemptedSecondDispostionOnAlreadySettledDelivery(Accepted.getInstance(), Released.getInstance());
+    }
+
+    @Test
+    public void testAttemptedSecondDispostionOnAlreadySettledDeliveryModiified() throws Exception {
+        doTestAttemptedSecondDispostionOnAlreadySettledDelivery(Released.getInstance(), new Modified().setDeliveryFailed(true));
+    }
+
+    @Test
+    public void testAttemptedSecondDispostionOnAlreadySettledDeliveryRejected() throws Exception {
+        doTestAttemptedSecondDispostionOnAlreadySettledDelivery(Released.getInstance(), new Rejected());
+    }
+
+    @Test
+    public void testAttemptedSecondDispostionOnAlreadySettledDeliveryTransactional() throws Exception {
+        doTestAttemptedSecondDispostionOnAlreadySettledDelivery(Released.getInstance(), new TransactionalState().setOutcome(Accepted.getInstance()));
+    }
+
+    private void doTestAttemptedSecondDispostionOnAlreadySettledDelivery(DeliveryState first, DeliveryState second) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0});
+        peer.expectDisposition().withFirst(0)
+                                .withSettled(true)
+                                .withState(notNullValue());
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        Sender sender = session.sender("sender-1");
+        final AtomicReference<OutgoingDelivery> sentDelivery = new AtomicReference<>();
+
+        final AtomicBoolean deliverySentAfterSenable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            sentDelivery.set(handler.next().setTag(new byte[] {0}).writeBytes(payload));
+            deliverySentAfterSenable.set(sender.isSendable());
+        });
+
+        sender.open();
+
+        assertTrue(deliverySentAfterSenable.get(), "Delivery should have been sent after credit arrived");
+
+        OutgoingDelivery delivery = sender.current();
+        assertNull(delivery);
+        sentDelivery.get().disposition(first, true);
+
+        // A second attempt at the same outcome should result in no action.
+        sentDelivery.get().disposition(first, true);
+
+        try {
+            sentDelivery.get().disposition(second, true);
+            fail("Should not be able to update outcome on already setttled delivery");
+        } catch (IllegalStateException ise) {
+            // Expected
+        }
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSettleSentDeliveryAfterRemoteSettles() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .accept();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        Sender sender = session.sender("sender-1");
+
+        final AtomicBoolean deliverySentAfterSenable = new AtomicBoolean();
+        final AtomicReference<OutgoingDelivery> sentDelivery = new AtomicReference<>();
+        sender.creditStateUpdateHandler(handler -> {
+            sentDelivery.set(handler.next().setTag(new byte[] {0}).writeBytes(payload));
+            deliverySentAfterSenable.set(sender.isSendable());
+        });
+
+        sender.deliveryStateUpdatedHandler((delivery) -> {
+            if (delivery.isRemotelySettled()) {
+                delivery.settle();
+            }
+        });
+
+        sender.open();
+
+        assertTrue(deliverySentAfterSenable.get(), "Delivery should have been sent after credit arrived");
+
+        assertNull(sender.current());
+
+        assertTrue(sentDelivery.get().isRemotelySettled());
+        assertSame(Accepted.getInstance(), sentDelivery.get().getRemoteState());
+        assertNull(sentDelivery.get().getState());
+        assertTrue(sentDelivery.get().isSettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderHandlesDeferredOpenAndBeginAttachResponses() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.expectBegin();
+        peer.expectAttach().withRole(Role.SENDER.getValue())
+                           .withTarget().withDynamic(true).withAddress((String) null);
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.setTarget(new Target().setDynamic(true).setAddress(null));
+        sender.openHandler(result -> senderRemotelyOpened.set(true)).open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        // This should happen after we inject the held open and attach
+        peer.expectClose().respond();
+
+        // Inject held responses to get the ball rolling again
+        peer.remoteOpen().withOfferedCapabilities("ANONYMOUS_REALY").now();
+        peer.respondToLastBegin().now();
+        peer.respondToLastAttach().now();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+        assertNotNull(sender.<Target>getRemoteTarget().getAddress());
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, true, true);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndNoRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenAndResponseBeginWrittenAndNoRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, false, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, false, false, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenNotWritten() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(false, false, false, false);
+    }
+
+    private void testCloseAfterShutdownNoOutputAndNoException(boolean respondToHeader, boolean respondToOpen, boolean respondToBegin, boolean respondToAttach) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                if (respondToBegin) {
+                    peer.expectBegin().respond();
+                    if (respondToAttach) {
+                        peer.expectAttach().respond();
+                    } else {
+                        peer.expectAttach();
+                    }
+                } else {
+                    peer.expectBegin();
+                    peer.expectAttach();
+                }
+            } else {
+                peer.expectOpen();
+                peer.expectBegin();
+                peer.expectAttach();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.open();
+
+        engine.shutdown();
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        sender.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndReponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true, true);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenAndBeginWrittenAndResponseAttachWrittenAndNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenAndResponseBeginWrittenAndNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, false, false, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenNotWritten() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(false, false, false, false);
+    }
+
+    private void testCloseAfterEngineFailedThrowsAndNoOutputWritten(boolean respondToHeader, boolean respondToOpen, boolean respondToBegin, boolean respondToAttach) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                if (respondToBegin) {
+                    peer.expectBegin().respond();
+                    if (respondToAttach) {
+                        peer.expectAttach().respond();
+                    } else {
+                        peer.expectAttach();
+                    }
+                } else {
+                    peer.expectBegin();
+                    peer.expectAttach();
+                }
+                peer.expectClose();
+            } else {
+                peer.expectOpen();
+                peer.expectBegin();
+                peer.expectAttach();
+                peer.expectClose();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("test");
+        sender.open();
+
+        engine.engineFailed(new IOException());
+
+        try {
+            sender.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        try {
+            session.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        try {
+            connection.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        engine.shutdown();  // Explicit shutdown now allows local close to complete
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        sender.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testCloseReceiverWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(true);
+    }
+
+    @Test
+    public void testDetachReceiverWithErrorCondition() throws Exception {
+        doTestCloseOrDetachWithErrorCondition(false);
+    }
+
+    private void doTestCloseOrDetachWithErrorCondition(boolean close) throws Exception {
+        final String condition = "amqp:link:detach-forced";
+        final String description = "something bad happened.";
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.expectDetach().withClosed(close)
+                           .withError(condition, description)
+                           .respond();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+        sender.setCondition(new ErrorCondition(Symbol.valueOf(condition), description));
+
+        if (close) {
+            sender.close();
+        } else {
+            sender.detach();
+        }
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderDrainedWhenNotDraining() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(10).withDrain(false).queue();
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.creditStateUpdateHandler(link -> link.drained());
+        sender.open();
+
+        assertEquals(10, sender.getCredit());
+
+        sender.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderDrainedWhenDrainSignaledButNoCreditGiven() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(0).withDrain(false).queue();
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.creditStateUpdateHandler(link -> link.drained());
+        sender.open();
+
+        assertEquals(0, sender.getCredit());
+
+        sender.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderSignalsDrainedWhenCreditOutstanding() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(10).withDrain(true).queue();
+        peer.expectFlow().withDeliveryCount(10).withLinkCredit(0).withDrain(true);
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.creditStateUpdateHandler(link -> link.drained());
+        sender.open();
+        sender.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderOmitsFlowWhenDrainedCreditIsSatisfied() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(1).withDrain(true).queue();
+
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .accept();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        final AtomicBoolean deliverySentAfterSenable = new AtomicBoolean();
+        final AtomicReference<OutgoingDelivery> sentDelivery = new AtomicReference<>();
+        sender.creditStateUpdateHandler(link -> {
+            if (link.isSendable()) {
+                sentDelivery.set(link.next().setTag(new byte[] {0}).writeBytes(payload));
+                deliverySentAfterSenable.set(true);
+            }
+        });
+
+        sender.deliveryStateUpdatedHandler((delivery) -> {
+            if (delivery.isRemotelySettled()) {
+                delivery.settle();
+            }
+        });
+
+        sender.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        // Should not send a flow as the send fulfilled the requested drain amount.
+        sender.drained();
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderAppliesDeliveryTagGeneratorToNextDelivery() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withIncomingWindow(10).withLinkCredit(10).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.setDeliveryTagGenerator(ProtonDeliveryTagGenerator.BUILTIN.SEQUENTIAL.createGenerator());
+        sender.deliveryStateUpdatedHandler((delivery) -> {
+            delivery.settle();
+        });
+
+        sender.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload()
+                             .withDeliveryTag(new byte[] {0}).accept();
+        peer.expectTransfer().withNonNullPayload()
+                             .withDeliveryTag(new byte[] {1}).accept();
+        peer.expectTransfer().withNonNullPayload()
+                             .withDeliveryTag(new byte[] {2}).accept();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(payload.duplicate());
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.writeBytes(payload.duplicate());
+        OutgoingDelivery delivery3 = sender.next();
+        delivery3.writeBytes(payload.duplicate());
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        assertNotNull(delivery1);
+        assertTrue(delivery1.isSettled());
+        assertTrue(delivery1.isRemotelySettled());
+        assertNotNull(delivery2);
+        assertTrue(delivery2.isSettled());
+        assertTrue(delivery2.isRemotelySettled());
+        assertNotNull(delivery3);
+        assertTrue(delivery3.isSettled());
+        assertTrue(delivery3.isRemotelySettled());
+
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderAppliedGeneratedDeliveryTagCanBeOverriden() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte [] payloadBuffer = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(payloadBuffer);
+
+        Sender sender = session.sender("sender-1");
+
+        assertFalse(sender.isSendable());
+
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        sender.setDeliveryTagGenerator(generator);
+        sender.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {127})
+                             .withPayload(payloadBuffer);
+        peer.expectDetach().withHandle(0).respond();
+
+        OutgoingDelivery delivery = sender.next();
+
+        DeliveryTag oldTag = delivery.getTag();
+
+        delivery.setTag(new byte[] {127});
+
+        // Pooled tag should be reused.
+        assertSame(oldTag, generator.nextTag());
+
+        delivery.writeBytes(payload);
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderReleasesPooledDeliveryTagsAfterSettledByBoth() throws Exception {
+        doTestSenderReleasesPooledDeliveryTags(false, true);
+    }
+
+    @Test
+    public void testSenderReleasesPooledDeliveryTagsAfterSettledAfterDispositionUpdate() throws Exception {
+        doTestSenderReleasesPooledDeliveryTags(false, false);
+    }
+
+    @Test
+    public void testSenderReleasesPooledDeliveryTagsSenderSettlesFirst() throws Exception {
+        doTestSenderReleasesPooledDeliveryTags(true, false);
+    }
+
+    private void doTestSenderReleasesPooledDeliveryTags(boolean sendSettled, boolean receiverSettles) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(10).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.setDeliveryTagGenerator(ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator());
+        sender.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        final int toSend = sender.getCredit();
+        final byte[] expectedTag = new byte[] {0};
+
+        List<OutgoingDelivery> sent = new ArrayList<>(toSend);
+
+        for (int i = 0; i < toSend; ++i) {
+            peer.expectTransfer().withHandle(0)
+                                 .withSettled(sendSettled)
+                                 .withState(nullValue())
+                                 .withDeliveryId(i)
+                                 .withMore(false)
+                                 .withDeliveryTag(expectedTag)
+                                 .respond()
+                                 .withSettled(receiverSettles)
+                                 .withState().accepted();
+            if (!sendSettled && !receiverSettles) {
+                peer.expectDisposition().withFirst(i)
+                                        .withSettled(true)
+                                        .withState(nullValue());
+            }
+        }
+
+        for (int i = 0; i < toSend; ++i) {
+            OutgoingDelivery delivery = sender.next();
+
+            if (sendSettled) {
+                delivery.settle();
+            }
+
+            delivery.writeBytes(payload.duplicate());
+
+            if (!sendSettled) {
+                delivery.settle();
+            }
+        }
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        sent.forEach(delivery -> assertEquals(delivery.getTag().tagBytes() , expectedTag));
+
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        // Should not send a flow as the send fulfilled the requested drain amount.
+        sender.drained();
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderHandlesDelayedDispositionsForSentTransfers() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withLinkCredit(10).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.setDeliveryTagGenerator(ProtonDeliveryTagGenerator.BUILTIN.SEQUENTIAL.createGenerator());
+        sender.deliveryStateUpdatedHandler((delivery) -> {
+            delivery.settle();
+        });
+
+        sender.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload()
+                             .withDeliveryTag(new byte[] {0});
+        peer.expectTransfer().withNonNullPayload()
+                             .withDeliveryTag(new byte[] {1});
+        peer.expectTransfer().withNonNullPayload()
+                             .withDeliveryTag(new byte[] {2});
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(payload.duplicate());
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.writeBytes(payload.duplicate());
+        OutgoingDelivery delivery3 = sender.next();
+        delivery3.writeBytes(payload.duplicate());
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        assertNotNull(delivery1);
+        assertNotNull(delivery2);
+        assertNotNull(delivery3);
+
+        peer.remoteDisposition().withRole(Role.RECEIVER.getValue())
+                                .withFirst(0)
+                                .withSettled(true)
+                                .withState().accepted().now();
+
+        assertTrue(delivery1.isSettled());
+        assertTrue(delivery1.isRemotelySettled());
+        assertFalse(delivery2.isSettled());
+        assertFalse(delivery2.isRemotelySettled());
+        assertFalse(delivery3.isSettled());
+        assertFalse(delivery3.isRemotelySettled());
+
+        peer.remoteDisposition().withRole(Role.RECEIVER.getValue())
+                                .withFirst(1)
+                                .withSettled(true)
+                                .withState().accepted().now();
+
+        assertTrue(delivery1.isSettled());
+        assertTrue(delivery1.isRemotelySettled());
+        assertTrue(delivery2.isSettled());
+        assertTrue(delivery2.isRemotelySettled());
+        assertFalse(delivery3.isSettled());
+        assertFalse(delivery3.isRemotelySettled());
+
+        peer.remoteDisposition().withRole(Role.RECEIVER.getValue())
+                                .withFirst(2)
+                                .withSettled(true)
+                                .withState().accepted().now();
+
+        assertTrue(delivery1.isSettled());
+        assertTrue(delivery1.isRemotelySettled());
+        assertTrue(delivery2.isSettled());
+        assertTrue(delivery2.isRemotelySettled());
+        assertTrue(delivery3.isSettled());
+        assertTrue(delivery3.isRemotelySettled());
+
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testNoDispsotionSentWhenNoStateOrSettlementRequested() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender").open();
+
+        final AtomicBoolean sender1MarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            sender1MarkedSendable.set(handler.isSendable());
+        });
+
+        peer.remoteFlow().withHandle(0)
+                         .withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(1)
+                         .withNextOutgoingId(1).now();
+
+        assertTrue(sender1MarkedSendable.get(), "Sender 1 should now be sendable");
+
+        // Frames are not multiplexed for large deliveries as we write the full
+        // writable portion out when a write is called.
+
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withNullState()
+                             .withDeliveryId(0)
+                             .withMore(false)
+                             .withDeliveryTag(new byte[] {1});
+
+        ProtonBuffer messageContent1 = createContentBuffer(32);
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.setTag(new byte[] { 1 });
+        delivery1.writeBytes(messageContent1);
+
+        // No action requested so no frame should be emitted.
+        delivery1.disposition(null, false);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().accepted();
+
+        delivery1.disposition(Accepted.getInstance(), true);
+
+        peer.expectClose().respond();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotAlterMessageFormatAfterInitalBytesWritten() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withMessageFormat(42)
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withMessageFormat(42)
+                             .withAborted(anyOf(nullValue(), is(false)))
+                             .withSettled(false)
+                             .withMore(anyOf(nullValue(), is(false)))
+                             .withPayload(payload);
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        final OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.setMessageFormat(42);
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), false);
+
+        assertThrows(IllegalStateException.class, () -> delivery.setMessageFormat(43));
+        assertDoesNotThrow(() -> delivery.setMessageFormat(42));
+
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), true);
+
+        assertFalse(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertFalse(delivery.isSettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCanUpdateAcceptedStateAfterInitialTransferAndSettle() throws Exception {
+        doTestCanUpdateStateAndOrSettleAfterInitialTransfer(true);
+    }
+
+    @Test
+    public void testCanUpdateAcceptedStateAfterInitialTransferDoNotSettle() throws Exception {
+        doTestCanUpdateStateAndOrSettleAfterInitialTransfer(false);
+    }
+
+    private void doTestCanUpdateStateAndOrSettleAfterInitialTransfer(boolean settle) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(1024)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withMessageFormat(42)
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withState().accepted()
+                             .withDeliveryId(0)
+                             .withMessageFormat(42)
+                             .withAborted(anyOf(nullValue(), is(false)))
+                             .withSettled(settle)
+                             .withMore(anyOf(nullValue(), is(false)))
+                             .withPayload(payload);
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        Sender sender = session.sender("sender-1");
+        sender.open();
+
+        final AtomicBoolean senderMarkedSendable = new AtomicBoolean();
+        sender.creditStateUpdateHandler(handler -> {
+            senderMarkedSendable.set(sender.isSendable());
+        });
+
+        final OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.setMessageFormat(42);
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), false);
+
+        assertThrows(IllegalStateException.class, () -> delivery.setMessageFormat(43));
+        assertDoesNotThrow(() -> delivery.setMessageFormat(42));
+
+        delivery.disposition(Accepted.getInstance(), settle);
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), true);
+
+        assertFalse(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        if (settle) {
+            assertTrue(delivery.isSettled());
+        } else {
+            assertFalse(delivery.isSettled());
+        }
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderNotSendableWhenRemoteIncomingWindowIsZero() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(0)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+
+        final OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), false);
+
+        assertFalse(sender.isSendable());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderBecomesSendableAfterRemoteIncomingWindowExpeanded() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(10)
+                         .withIncomingWindow(0)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(1).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        {
+            // Not expecting an update as we weren't yet able to send and still aren't
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            sender.open();
+
+            assertTrue(senderCreditUpdated.await(5, TimeUnit.MILLISECONDS));
+            assertFalse(sender.isSendable());
+        }
+
+        final OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        // Shouldn't generate any frames as there's no session capacity
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), false);
+
+        {
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(10)
+                             .withIncomingWindow(1)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(0).now();
+
+            assertTrue(senderCreditUpdated.await(10, TimeUnit.SECONDS));
+            assertTrue(sender.isSendable());
+        }
+
+        peer.expectTransfer().withHandle(0)
+                             .withMore(false)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectDetach().withHandle(0).respond();
+
+        delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        assertFalse(sender.isSendable());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderBecomesSendableAfterRemoteIncomingWindowExpeandedSessionFlowSentBeforeAttach() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteFlow().withNullHandle()
+                         .withIncomingWindow(0)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(0).queue();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+
+        assertFalse(sender.isSendable());
+
+        final OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        delivery.setTag(new byte[] {0});
+        // Shouldn't generate any frames as there's no session capacity
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload), false);
+
+        {
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(10)
+                             .withIncomingWindow(1)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(0).now();
+
+            assertTrue(senderCreditUpdated.await(10, TimeUnit.SECONDS));
+            assertTrue(sender.isSendable());
+        }
+
+        peer.expectTransfer().withHandle(0)
+                             .withMore(false)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectDetach().withHandle(0).respond();
+
+        delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        assertFalse(sender.isSendable());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionRevokesIncomingWindowSetsSenderStateToNotSenableViaDirectLinkFlow() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteFlow().withIncomingWindow(1)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(0).queue();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+
+        assertFalse(sender.isSendable());
+
+        {
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(1)
+                             .withIncomingWindow(1)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(0).now();
+
+            assertTrue(senderCreditUpdated.await(10, TimeUnit.SECONDS));
+            assertTrue(sender.isSendable());
+        }
+
+        {
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(1)
+                             .withIncomingWindow(0)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(0).now();
+
+            assertTrue(senderCreditUpdated.await(10, TimeUnit.SECONDS));
+            assertFalse(sender.isSendable());
+        }
+
+        peer.expectDetach().withHandle(0).respond();
+
+        // Should not generate any outgoing transfers as the delivery is not sendable
+        final OutgoingDelivery delivery = sender.next();
+        delivery.setTag(new byte[] {0});
+        delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionRevokesIncomingWindowSetsSenderStateToNotSenableViaSessionFlow() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteFlow().withIncomingWindow(1)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(0).queue();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+
+        assertFalse(sender.isSendable());
+
+        {
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            peer.remoteFlow().withDeliveryCount(0)
+                             .withLinkCredit(1)
+                             .withIncomingWindow(1)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(0).now();
+
+            assertTrue(senderCreditUpdated.await(10, TimeUnit.SECONDS));
+            assertTrue(sender.isSendable());
+        }
+
+        {
+            final CountDownLatch senderCreditUpdated = new CountDownLatch(1);
+            sender.creditStateUpdateHandler(handler -> {
+                senderCreditUpdated.countDown();
+            });
+
+            // Arrives at session level but impacts the links in the session.
+            peer.remoteFlow().withNullHandle()
+                             .withIncomingWindow(0)
+                             .withOutgoingWindow(10)
+                             .withNextIncomingId(0)
+                             .withNextOutgoingId(0).now();
+
+            assertTrue(senderCreditUpdated.await(10, TimeUnit.SECONDS));
+            assertFalse(sender.isSendable());
+        }
+
+        peer.expectDetach().withHandle(0).respond();
+
+        // Should not generate any outgoing transfers as the delivery is not sendable
+        final OutgoingDelivery delivery = sender.next();
+        delivery.setTag(new byte[] {0});
+        delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderOnlyWritesToSessionRemoteIncomingLimitWriteBytes() throws Exception {
+        doTestSenderOnlyWritesToSessionRemoteIncomingLimit(false);
+    }
+
+    @Test
+    public void testSenderOnlyWritesToSessionRemoteIncomingLimitStreamBytes() throws Exception {
+        doTestSenderOnlyWritesToSessionRemoteIncomingLimit(true);
+    }
+
+    private void doTestSenderOnlyWritesToSessionRemoteIncomingLimit(boolean streamBytes) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[1536];
+        Arrays.fill(payload, (byte) 64);
+        ProtonBuffer payloadBuffer = ProtonByteBufferAllocator.DEFAULT.wrap(payload);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver").withMaxFrameSize(1024);
+        peer.expectBegin().respond().withIncomingWindow(1);
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0)
+                         .withLinkCredit(1)
+                         .withIncomingWindow(1)
+                         .withOutgoingWindow(10)
+                         .withNextIncomingId(0)
+                         .withNextOutgoingId(0).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withNonNullPayload();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+
+        final OutgoingDelivery delivery = sender.next();
+
+        delivery.setTag(new byte[] {0});
+        if (streamBytes) {
+            delivery.streamBytes(payloadBuffer, true);
+        } else {
+            delivery.writeBytes(payloadBuffer);
+        }
+
+        assertTrue(delivery.isPartial());
+        assertTrue(payloadBuffer.isReadable());
+        assertNotEquals(payload.length, payloadBuffer.getReadableBytes());
+        assertFalse(sender.isSendable());
+
+        peer.remoteFlow().withIncomingWindow(1)
+                         .withNextIncomingId(1)
+                         .withLinkCredit(10).now();
+
+        assertTrue(sender.isSendable());
+
+        peer.expectTransfer().withHandle(0)
+                             .withMore(false)
+                             .withDeliveryId(0)
+                             .withNonNullPayload();
+        peer.expectDetach().withHandle(0).respond();
+
+        if (streamBytes) {
+            delivery.streamBytes(payloadBuffer, true);
+        } else {
+            delivery.writeBytes(payloadBuffer);
+        }
+
+        assertFalse(delivery.isPartial());
+        assertFalse(payloadBuffer.isReadable());
+        assertFalse(sender.isSendable());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderUpdateDeliveryUpdatedEventHandlerInDelivery() throws InterruptedException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().ofSender().respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(1).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withSettled(false)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .respond()
+                             .withSettled(true)
+                             .withState().accepted();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        final CountDownLatch stateUpdated = new CountDownLatch(1);
+        sender.creditStateUpdateHandler(link -> {
+            if (link.isSendable()) {
+                OutgoingDelivery delivery = sender.next();
+                delivery.deliveryStateUpdatedHandler((outgoing) -> {
+                    stateUpdated.countDown();
+                });
+
+                delivery.setTag(new byte[] {0});
+                delivery.writeBytes(payload);
+            }
+        });
+
+        sender.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        assertTrue(stateUpdated.await(5, TimeUnit.SECONDS));
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testTransferCountTracksOutgoingDeliveryLifecycle() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withDeliveryCount(0).withLinkCredit(10).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withMore(true)
+                             .withDeliveryId(0)
+                             .withDeliveryTag(anyOf(nullValue(), is(new byte[] {0})))
+                             .withPayload(payload);
+        peer.expectTransfer().withHandle(0)
+                             .withState(nullValue())
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withAborted(true)
+                             .withMore(anyOf(nullValue(), is(false)))
+                             .withNullPayload();
+        peer.expectDetach().withHandle(0).respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1").open();
+
+        OutgoingDelivery delivery = sender.next();
+        assertNotNull(delivery);
+
+        assertEquals(0, delivery.getTransferCount());
+
+        delivery.setTag(new byte[] {0});
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        assertEquals(1, delivery.getTransferCount());
+
+        delivery.streamBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        assertEquals(2, delivery.getTransferCount());
+
+        delivery.abort();
+
+        assertEquals(2, delivery.getTransferCount());
+
+        assertTrue(delivery.isAborted());
+        assertFalse(delivery.isPartial());
+        assertTrue(delivery.isSettled());
+
+        sender.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDispositionFilterAppliesToOnlySubsetOfUnsttledMapSettledAndAccepted() {
+        testDispositionFilterAppliesToOnlySubsetOfUnsttledMap(true, true);
+    }
+
+    @Test
+    public void testDispositionFilterAppliesToOnlySubsetOfUnsttledMapSettledOnly() {
+        testDispositionFilterAppliesToOnlySubsetOfUnsttledMap(true, false);
+    }
+
+    @Test
+    public void testDispositionFilterAppliesToOnlySubsetOfUnsttledMapAcceptedOnly() {
+        testDispositionFilterAppliesToOnlySubsetOfUnsttledMap(false, true);
+    }
+
+    private void testDispositionFilterAppliesToOnlySubsetOfUnsttledMap(boolean settled, boolean accepted) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {0, 1, 2, 3, 4});
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(10).queue();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(false)
+                             .withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withNonNullPayload();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(false)
+                             .withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {1})
+                             .withNonNullPayload();
+        peer.expectTransfer().withHandle(0)
+                             .withMore(false)
+                             .withDeliveryId(2)
+                             .withDeliveryTag(new byte[] {2})
+                             .withNonNullPayload();
+        if (!accepted) {
+            peer.expectDisposition().withFirst(1).withSettled(settled).withState(nullValue());
+        } else {
+            peer.expectDisposition().withFirst(1).withSettled(settled).withState().accepted();
+        }
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("sender-1");
+
+        sender.creditStateUpdateHandler(link -> link.drained());
+        sender.open();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.setTag(new byte[] { 0 });
+        delivery1.writeBytes(payload.duplicate());
+
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.setTag(new byte[] { 1 });
+        delivery2.writeBytes(payload.duplicate());
+
+        OutgoingDelivery delivery3 = sender.next();
+        delivery3.setTag(new byte[] { 2 });
+        delivery3.writeBytes(payload.duplicate());
+
+        sender.disposition((delivery) -> {
+            if (delivery.getTag().tagBuffer().equals(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {1}))) {
+                return true;
+            } else {
+                return false;
+            }
+        }, accepted ? Accepted.getInstance() : null, settled);
+
+        assertEquals(7, sender.getCredit());
+
+        sender.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSenderReportsDeliveryUpdatedOnDispositionForMultipleTransfers() throws Exception {
+        final Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        final ProtonTestConnector peer = createTestPeer(engine);
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withPayload(payload);
+        peer.expectTransfer().withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {1})
+                             .withMore(false)
+                             .withPayload(payload);
+        peer.remoteDisposition().withSettled(true)
+                                .withRole(Role.RECEIVER.getValue())
+                                .withState().accepted()
+                                .withFirst(0)
+                                .withLast(1).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test");
+
+        final AtomicInteger dispositionCounter = new AtomicInteger();
+
+        final ArrayList<OutgoingDelivery> deliveries = new ArrayList<>();
+
+        sender.deliveryStateUpdatedHandler(delivery -> {
+            if (delivery.isRemotelySettled()) {
+                dispositionCounter.incrementAndGet();
+                deliveries.add(delivery);
+            }
+        });
+
+        sender.open();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.setTag(new byte[] { 0 });
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.setTag(new byte[] { 1 });
+        delivery2.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().respond();
+
+        sender.close();
+
+        assertEquals(2, deliveries.size(), "Not all deliveries received dispositions");
+
+        byte deliveryTag = 0;
+
+        for (OutgoingDelivery delivery : deliveries) {
+            assertEquals(deliveryTag++, delivery.getTag().tagBuffer().getByte(0), "Delivery not updated in correct order");
+            assertTrue(delivery.isRemotelySettled(), "Delivery should be marked as remotely setted");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderReportsIsSendableAfterOpenedIfRemoteSendsFlowBeforeLocallyOpened() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("receiver")
+                           .withHandle(0)
+                           .withRole(Role.RECEIVER.getValue())
+                           .withInitialDeliveryCount(0)
+                           .onChannel(0).queue();
+        peer.remoteFlow().withLinkCredit(1).queue();
+        peer.expectAttach();
+        peer.expectDetach().respond();
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+        final AtomicReference<Sender> sender = new AtomicReference<>();
+
+        Connection connection = engine.start();
+
+        connection.senderOpenHandler(result -> {
+            senderRemotelyOpened.set(true);
+            sender.set(result);
+        });
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        connection.session().open();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender remote opened event did not fire");
+
+        assertFalse(sender.get().isSendable());
+
+        sender.get().open();
+
+        assertTrue(sender.get().isSendable());
+
+        sender.get().close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    void testWriteThatExceedConfiguredSessionIncomingCreditLimitOnTransfer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().withNextOutgoingId(0).respond();
+        peer.expectAttach().ofSender().respond();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        int payloadOutstanding = 4800;
+        final byte[] bytes = new byte[payloadOutstanding];
+        Arrays.fill(bytes, (byte) 1);
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(bytes);
+
+        OutgoingDelivery delivery = sender.next().setTag(new byte[] { 0 });
+        assertEquals(payload.getReadableBytes(), payloadOutstanding);
+        delivery.writeBytes(payload);
+        assertEquals(payload.getReadableBytes(), payloadOutstanding);
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(0).withLinkCredit(10).now();
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+
+        delivery.writeBytes(payload);
+        assertTrue(payload.getReadableBytes() < payloadOutstanding);  // Leave space for Transfer
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(1).withLinkCredit(10).now();
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+
+        delivery.writeBytes(payload);
+        assertTrue(payload.getReadableBytes() < payloadOutstanding, "Expected < " + payloadOutstanding + " but was: " + payload.getReadableBytes());
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).now();
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+
+        delivery.writeBytes(payload);
+        assertTrue(payload.getReadableBytes() < payloadOutstanding, "Expected < " + payloadOutstanding + " but was: " + payload.getReadableBytes());
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).now();
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+
+        delivery.writeBytes(payload);
+        assertTrue(payload.getReadableBytes() < payloadOutstanding, "Expected < " + payloadOutstanding + " but was: " + payload.getReadableBytes());
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).now();
+        peer.expectTransfer().withNonNullPayload().withMore(false).accept();
+
+        delivery.writeBytes(payload);
+        assertEquals(0 , payload.getReadableBytes());
+
+        peer.waitForScriptToComplete(500, TimeUnit.SECONDS);
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    void testWriteThatExceedsConfiguredSessionIncomingCreditLimitOnTransferFromCreditUpdatedhandler() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().withNextOutgoingId(0).respond();
+        peer.expectAttach().ofSender().respond();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        int payloadOutstanding = 4800;
+        final byte[] bytes = new byte[payloadOutstanding];
+        Arrays.fill(bytes, (byte) 1);
+        ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(bytes);
+
+        final OutgoingDelivery delivery = sender.next().setTag(new byte[] { 0 });
+        assertEquals(payload.getReadableBytes(), payloadOutstanding);
+        delivery.writeBytes(payload);
+        assertEquals(payload.getReadableBytes(), payloadOutstanding);
+
+        sender.creditStateUpdateHandler((theSender) -> {
+            delivery.writeBytes(payload);
+        });
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(0).withLinkCredit(10).now();
+
+        assertTrue(payload.getReadableBytes() < payloadOutstanding);  // Leave space for Transfer
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(1).withLinkCredit(10).now();
+
+        assertTrue(payload.getReadableBytes() < payloadOutstanding, "Expected < " + payloadOutstanding + " but was: " + payload.getReadableBytes());
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(2).withLinkCredit(10).now();
+
+        assertTrue(payload.getReadableBytes() < payloadOutstanding, "Expected < " + payloadOutstanding + " but was: " + payload.getReadableBytes());
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload().withMore(true);
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(3).withLinkCredit(10).now();
+
+        assertTrue(payload.getReadableBytes() < payloadOutstanding, "Expected < " + payloadOutstanding + " but was: " + payload.getReadableBytes());
+        payloadOutstanding = payload.getReadableBytes();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectTransfer().withNonNullPayload().withMore(false).accept();
+        peer.remoteFlow().withIncomingWindow(1).withNextIncomingId(4).withLinkCredit(10).now();
+
+        assertEquals(0 , payload.getReadableBytes());
+
+        peer.waitForScriptToComplete(500, TimeUnit.SECONDS);
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        sender.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSequentialTagGeneratorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSequentialTagGeneratorTest.java
new file mode 100644
index 0000000..fc9ccfd
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSequentialTagGeneratorTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.junit.jupiter.api.Test;
+
+public class ProtonSequentialTagGeneratorTest {
+
+    @Test
+    public void testCreateTagGenerator() {
+        DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.SEQUENTIAL.createGenerator();
+        assertTrue(generator instanceof ProtonSequentialTagGenerator);
+        assertNotNull(generator.toString());
+    }
+
+    @Test
+    public void testCreateTag() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+
+        assertNotNull(generator.nextTag());
+    }
+
+    @Test
+    public void testCopyTag() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+        DeliveryTag next = generator.nextTag();
+        DeliveryTag copy = next.copy();
+
+        assertNotSame(next, copy);
+        assertEquals(next, copy);
+    }
+
+    @Test
+    public void testTagEquals() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+
+        DeliveryTag tag1 = generator.nextTag();
+        DeliveryTag tag2 = generator.nextTag();
+        DeliveryTag tag3 = generator.nextTag();
+
+        assertEquals(tag1, tag1);
+        assertNotEquals(tag1, tag2);
+        assertNotEquals(tag2, tag3);
+        assertNotEquals(tag1, tag3);
+
+        assertNotEquals(null, tag1);
+        assertNotEquals(tag1, null);
+        assertNotEquals("something", tag1);
+        assertNotEquals(tag2, "something");
+    }
+
+    @Test
+    public void testCreateTagsThatAreEqual() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+
+        generator.setNextTagId(42);
+        DeliveryTag tag1 = generator.nextTag();
+
+        generator.setNextTagId(42);
+        DeliveryTag tag2 = generator.nextTag();
+
+        assertNotSame(tag1, tag2);
+        assertEquals(tag1, tag2);
+
+        assertEquals(tag1.hashCode(), tag2.hashCode());
+    }
+
+    @Test
+    public void testCreateTagsThatWrapAroundLimit() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+
+        // Test that on wrap the tags start beyond the cached values.
+        generator.setNextTagId(0xFFFFFFFFFFFFFFFFl);
+
+        DeliveryTag maxUnsignedLong = generator.nextTag();
+        DeliveryTag nextTagAfterWrap = generator.nextTag();
+
+        assertEquals(Long.BYTES, maxUnsignedLong.tagBytes().length);
+        assertEquals(Byte.BYTES, nextTagAfterWrap.tagBytes().length);
+    }
+
+    @Test
+    public void testCreateMatchingValuesFromWrittenBuffer() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(64);
+
+        generator.setNextTagId(-127);                // Long
+        generator.nextTag().writeTo(buffer);
+        generator.setNextTagId(127);                 // Byte
+        generator.nextTag().writeTo(buffer);
+        generator.setNextTagId(256);                 // Short
+        generator.nextTag().writeTo(buffer);
+        generator.setNextTagId(65536);               // Int
+        generator.nextTag().writeTo(buffer);
+        generator.setNextTagId(0x00000001FFFFFFFFl); // Long
+        generator.nextTag().writeTo(buffer);
+
+        assertEquals(23, buffer.getReadableBytes());
+
+        assertEquals(-127, buffer.readLong());
+        assertEquals(127, buffer.readByte());
+        assertEquals(256, buffer.readShort());
+        assertEquals(65536, buffer.readInt());
+        assertEquals(0x00000001FFFFFFFFl, buffer.readLong());
+    }
+
+    @Test
+    public void testTagSizeMatchesValueRange() {
+        ProtonSequentialTagGenerator generator = new ProtonSequentialTagGenerator();
+
+        generator.setNextTagId(-127);
+        assertEquals(Long.BYTES, generator.nextTag().tagLength());
+        assertEquals(Long.BYTES, generator.nextTag().tagBytes().length);
+        assertEquals(Long.BYTES, generator.nextTag().tagBuffer().getReadableBytes());
+
+        generator.setNextTagId(127);
+        assertEquals(Byte.BYTES, generator.nextTag().tagLength());
+        assertEquals(Byte.BYTES, generator.nextTag().tagBytes().length);
+        assertEquals(Byte.BYTES, generator.nextTag().tagBuffer().getReadableBytes());
+
+        generator.setNextTagId(256);
+        assertEquals(Short.BYTES, generator.nextTag().tagLength());
+        assertEquals(Short.BYTES, generator.nextTag().tagBytes().length);
+        assertEquals(Short.BYTES, generator.nextTag().tagBuffer().getReadableBytes());
+
+        generator.setNextTagId(65536);
+        assertEquals(Integer.BYTES, generator.nextTag().tagLength());
+        assertEquals(Integer.BYTES, generator.nextTag().tagBytes().length);
+        assertEquals(Integer.BYTES, generator.nextTag().tagBuffer().getReadableBytes());
+
+        generator.setNextTagId(0x00000001FFFFFFFFl);
+        assertEquals(Long.BYTES, generator.nextTag().tagLength());
+        assertEquals(Long.BYTES, generator.nextTag().tagBytes().length);
+        assertEquals(Long.BYTES, generator.nextTag().tagBuffer().getReadableBytes());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionTest.java
new file mode 100644
index 0000000..2fe6464
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonSessionTest.java
@@ -0,0 +1,2590 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.ConnectionState;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.IncomingDelivery;
+import org.apache.qpid.protonj2.engine.Link;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStateException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.test.driver.matchers.types.UnsignedIntegerMatcher;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ConnectionError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.apache.qpid.protonj2.types.transport.SessionError;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test behaviors of the ProtonSession implementation.
+ */
+@Timeout(20)
+public class ProtonSessionTest extends ProtonEngineTestSupport {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonSessionTest.class);
+
+    @Test
+    public void testSessionEmitsOpenAndCloseEvents() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean sessionLocalOpen = new AtomicBoolean();
+        final AtomicBoolean sessionLocalClose = new AtomicBoolean();
+        final AtomicBoolean sessionRemoteOpen = new AtomicBoolean();
+        final AtomicBoolean sessionRemoteClose = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+
+        session.localOpenHandler(result -> sessionLocalOpen.set(true))
+               .localCloseHandler(result -> sessionLocalClose.set(true))
+               .openHandler(result -> sessionRemoteOpen.set(true))
+               .closeHandler(result -> sessionRemoteClose.set(true));
+
+        session.open();
+
+        assertEquals(connection, session.getParent());
+
+        session.close();
+
+        assertTrue(sessionLocalOpen.get(), "Session should have reported local open");
+        assertTrue(sessionLocalClose.get(), "Session should have reported local close");
+        assertTrue(sessionRemoteOpen.get(), "Session should have reported remote open");
+        assertTrue(sessionRemoteClose.get(), "Session should have reported remote close");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineShutdownEventNeitherEndClosed() throws Exception {
+        doTestEngineShutdownEvent(false, false);
+    }
+
+    @Test
+    public void testEngineShutdownEventLocallyClosed() throws Exception {
+        doTestEngineShutdownEvent(true, false);
+    }
+
+    @Test
+    public void testEngineShutdownEventRemotelyClosed() throws Exception {
+        doTestEngineShutdownEvent(false, true);
+    }
+
+    @Test
+    public void testEngineShutdownEventBothEndsClosed() throws Exception {
+        doTestEngineShutdownEvent(true, true);
+    }
+
+    private void doTestEngineShutdownEvent(boolean locallyClosed, boolean remotelyClosed) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean engineShutdown = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+        session.engineShutdownHandler(result -> engineShutdown.set(true));
+
+        if (locallyClosed) {
+            if (remotelyClosed) {
+                peer.expectEnd().respond();
+            } else {
+                peer.expectEnd();
+            }
+
+            session.close();
+        }
+
+        if (remotelyClosed && !locallyClosed) {
+            peer.remoteEnd();
+        }
+
+        engine.shutdown();
+
+        if (locallyClosed && remotelyClosed) {
+            assertFalse(engineShutdown.get(), "Should not have reported engine shutdown");
+        } else {
+            assertTrue(engineShutdown.get(), "Should have reported engine shutdown");
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionOpenAndCloseAreIdempotent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session();
+        session.open();
+
+        // Should not emit another begin frame
+        session.open();
+
+        session.close();
+
+        // Should not emit another end frame
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderCreateOnClosedSessionThrowsISE() throws Exception {
+        testLinkCreateOnClosedSessionThrowsISE(false);
+    }
+
+    @Test
+    public void testReceiverCreateOnClosedSessionThrowsISE() throws Exception {
+        testLinkCreateOnClosedSessionThrowsISE(true);
+    }
+
+    private void testLinkCreateOnClosedSessionThrowsISE(boolean receiver) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        Session session = connection.session().open().close();
+
+        if (receiver) {
+            try {
+                session.receiver("test");
+                fail("Should not allow receiver create on closed session");
+            } catch (IllegalStateException ise) {
+                // Expected
+            }
+        } else {
+            try {
+                session.sender("test");
+                fail("Should not allow sender create on closed session");
+            } catch (IllegalStateException ise) {
+                // Expected
+            }
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenSessionBeforeOpenConnection() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // An opened session shouldn't write its begin until the parent connection
+        // is opened and once it is the begin should be automatically written.
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.expectBegin();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineEmitsBeginAfterLocalSessionOpened() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+
+        final AtomicBoolean remoteOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.open();
+        connection.openHandler((result) -> {
+            remoteOpened.set(true);
+        });
+
+        Session session = connection.session();
+        session.open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionFiresOpenedEventAfterRemoteOpensLocallyOpenedSession() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+
+        final AtomicBoolean connectionRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean sessionRemotelyOpened = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        connection.openHandler((result) -> {
+            connectionRemotelyOpened.set(true);
+        });
+        connection.open();
+
+        assertTrue(connectionRemotelyOpened.get(), "Connection remote opened event did not fire");
+
+        Session session = connection.session();
+        session.openHandler(result -> {
+            sessionRemotelyOpened.set(true);
+        });
+        session.open();
+
+        assertTrue(sessionRemotelyOpened.get(), "Session remote opened event did not fire");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testNoSessionPerformativesEmiitedIfConnectionOpenedAndClosedBeforeAnyRemoteResponses() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // An opened session shouldn't write its begin until the parent connection
+        // is opened and once it is the begin should be automatically written.
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+
+        peer.expectAMQPHeader();
+
+        connection.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        connection.close();
+
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+        peer.remoteHeader(AMQPHeader.getAMQPHeader().toArray()).now();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenAndCloseSessionWithNullSetsOnSessionOptions() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().onChannel(0).respond();
+        peer.expectEnd().onChannel(0).respond();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.setProperties(null);
+        session.setOfferedCapabilities((Symbol[]) null);
+        session.setDesiredCapabilities((Symbol[]) null);
+        session.setCondition(null);
+        session.open();
+
+        assertNotNull(session.getAttachments());
+        assertNull(session.getProperties());
+        assertNull(session.getOfferedCapabilities());
+        assertNull(session.getDesiredCapabilities());
+        assertNull(session.getCondition());
+
+        assertNull(session.getRemoteProperties());
+        assertNull(session.getRemoteOfferedCapabilities());
+        assertNull(session.getRemoteDesiredCapabilities());
+        assertNull(session.getRemoteCondition());
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenAndCloseMultipleSessions() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().onChannel(0).respond();
+        peer.expectBegin().onChannel(1).respond();
+        peer.expectEnd().onChannel(1).respond();
+        peer.expectEnd().onChannel(0).respond();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session1 = connection.session();
+        session1.open();
+        Session session2 = connection.session();
+        session2.open();
+
+        session2.close();
+        session1.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineFireRemotelyOpenedSessionEventWhenRemoteBeginArrives() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.remoteBegin().queue();
+
+        final AtomicBoolean connectionRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean sessionRemotelyOpened = new AtomicBoolean();
+
+        final AtomicReference<Session> remoteSession = new AtomicReference<>();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler((result) -> {
+            connectionRemotelyOpened.set(true);
+        });
+        connection.sessionOpenHandler(result -> {
+            remoteSession.set(result);
+            sessionRemotelyOpened.set(true);
+        });
+        connection.open();
+
+        assertTrue(connectionRemotelyOpened.get(), "Connection remote opened event did not fire");
+        assertTrue(sessionRemotelyOpened.get(), "Session remote opened event did not fire");
+        assertNotNull(remoteSession.get(), "Connection did not create a local session for remote open");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionPopulatesBeginUsingDefaults() throws IOException {
+        doTestSessionOpenPopulatesBegin(false, false);
+    }
+
+    @Test
+    public void testSessionPopulatesBeginWithConfiguredMaxFrameSizeButNoIncomingCapacity() throws IOException {
+        doTestSessionOpenPopulatesBegin(true, false);
+    }
+
+    @Test
+    public void testSessionPopulatesBeginWithConfiguredMaxFrameSizeAndIncomingCapacity() throws IOException {
+        doTestSessionOpenPopulatesBegin(true, true);
+    }
+
+    private void doTestSessionOpenPopulatesBegin(boolean setMaxFrameSize, boolean setIncomingCapacity) {
+        final int MAX_FRAME_SIZE = 32767;
+        final int SESSION_INCOMING_CAPACITY = Integer.MAX_VALUE;
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final Matcher<?> expectedMaxFrameSize;
+
+        if (setMaxFrameSize) {
+            expectedMaxFrameSize = new UnsignedIntegerMatcher(MAX_FRAME_SIZE);
+        } else {
+            expectedMaxFrameSize = new UnsignedIntegerMatcher(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE);
+        }
+
+        int expectedIncomingWindow = Integer.MAX_VALUE;
+        if (setIncomingCapacity) {
+            expectedIncomingWindow = SESSION_INCOMING_CAPACITY / MAX_FRAME_SIZE;
+        }
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(expectedMaxFrameSize).respond().withContainerId("driver");
+        peer.expectBegin().withHandleMax(nullValue())
+                          .withNextOutgoingId(0)
+                          .withIncomingWindow(expectedIncomingWindow)
+                          .withOutgoingWindow(Integer.MAX_VALUE)
+                          .withOfferedCapabilities(nullValue())
+                          .withDesiredCapabilities(nullValue())
+                          .withProperties(nullValue())
+                          .respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+        if (setMaxFrameSize) {
+            connection.setMaxFrameSize(MAX_FRAME_SIZE);
+        }
+        connection.open();
+
+        Session session = connection.session();
+        if (setIncomingCapacity) {
+            session.setIncomingCapacity(SESSION_INCOMING_CAPACITY);
+        }
+        session.open();
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionOpenFailsWhenConnectionClosed() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectClose().respond();
+
+        final AtomicBoolean connectionOpenedSignaled = new AtomicBoolean();
+        final AtomicBoolean connectionClosedSignaled = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler(result -> {
+            connectionOpenedSignaled.set(true);
+        });
+        connection.closeHandler(result -> {
+            connectionClosedSignaled.set(true);
+        });
+
+        Session session = connection.session();
+        connection.open();
+        connection.close();
+
+        assertTrue(connectionOpenedSignaled.get(), "Connection remote opened event did not fire");
+        assertTrue(connectionClosedSignaled.get(), "Connection remote closed event did not fire");
+
+        try {
+            session.open();
+            fail("Should not be able to open a session when its Connection was already closed");
+        } catch (IllegalStateException ise) {}
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionOpenFailsWhenConnectionRemotelyClosed() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.remoteClose().queue();
+
+        final AtomicBoolean connectionOpenedSignaled = new AtomicBoolean();
+        final AtomicBoolean connectionClosedSignaled = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler(result -> {
+            connectionOpenedSignaled.set(true);
+        });
+        connection.closeHandler(result -> {
+            connectionClosedSignaled.set(true);
+        });
+
+        Session session = connection.session();
+        connection.open();
+
+        assertTrue(connectionOpenedSignaled.get(), "Connection remote opened event did not fire");
+        assertTrue(connectionClosedSignaled.get(), "Connection remote closed event did not fire");
+
+        try {
+            session.open();
+            fail("Should not be able to open a session when its Connection was already closed");
+        } catch (IllegalStateException ise) {}
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionOpenFailsWhenWriteOfBeginFailsWithException() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.dropAfterLastHandler();
+
+        Connection connection = engine.start().open();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+        assertTrue(connection.getState() == ConnectionState.ACTIVE);
+        assertTrue(connection.getRemoteState() == ConnectionState.ACTIVE);
+
+        Session session = connection.session();
+
+        try {
+            session.open();
+            fail("Should not be able to open a session when its Connection was already closed");
+        } catch (EngineFailedException failure) {
+            LOG.trace("Got expected engine failure from session Begin write.", failure);
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+        assertTrue(engine.isFailed());
+        assertFalse(engine.isShutdown());
+        assertNotNull(engine.failureCause());
+    }
+
+    @Test
+    public void testSessionOpenNotSentUntilConnectionOpened() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectClose();
+
+        connection.open();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionCloseNotSentUntilConnectionOpened() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean sessionOpenedSignaled = new AtomicBoolean();
+
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.openHandler(result -> sessionOpenedSignaled.set(true));
+        session.open();
+        session.close();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+        peer.expectClose();
+
+        assertFalse(sessionOpenedSignaled.get(), "Session opened handler should not have been called yet");
+
+        connection.open();
+
+        // Session was already closed so no open event should fire.
+        assertFalse(sessionOpenedSignaled.get(), "Session opened handler should not have been called yet");
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionRemotelyClosedWithError() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+        session.open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(session.isLocallyOpen());
+        assertFalse(session.isLocallyClosed());
+        assertTrue(session.isRemotelyOpen());
+        assertFalse(session.isRemotelyClosed());
+
+        peer.expectEnd();
+        peer.expectClose();
+        peer.remoteEnd().withErrorCondition(AmqpError.INTERNAL_ERROR.toString(), "Error").now();
+
+        assertTrue(session.isLocallyOpen());
+        assertFalse(session.isLocallyClosed());
+        assertFalse(session.isRemotelyOpen());
+        assertTrue(session.isRemotelyClosed());
+
+        assertEquals(AmqpError.INTERNAL_ERROR, session.getRemoteCondition().getCondition());
+        assertEquals("Error", session.getRemoteCondition().getDescription());
+
+        session.close();
+
+        assertFalse(session.isLocallyOpen());
+        assertTrue(session.isLocallyClosed());
+        assertFalse(session.isRemotelyOpen());
+        assertTrue(session.isRemotelyClosed());
+
+        assertEquals(AmqpError.INTERNAL_ERROR, session.getRemoteCondition().getCondition());
+        assertEquals("Error", session.getRemoteCondition().getDescription());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionCloseAfterConnectionRemotelyClosedWhenNoBeginResponseReceived() throws EngineStateException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Connection connection = engine.start();
+        Session session = connection.session();
+        session.open();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin();
+        peer.remoteClose().withErrorCondition(AmqpError.NOT_ALLOWED.toString(), "Error").queue();
+
+        connection.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectEnd();
+        peer.expectClose();
+
+        // Connection not locally closed so end still written.
+        session.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testHandleRemoteBeginWithInvalidRemoteChannelSet() throws IOException {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+
+        final AtomicBoolean remoteOpened = new AtomicBoolean();
+        final AtomicBoolean remoteSession = new AtomicBoolean();
+
+        Connection connection = engine.start();
+
+        // Default engine should start and return a connection immediately
+        assertNotNull(connection);
+
+        connection.openHandler((result) -> {
+            remoteOpened.set(true);
+        });
+        connection.open();
+
+        connection.sessionOpenHandler(session -> {
+            remoteSession.set(true);
+        });
+
+        peer.waitForScriptToComplete();
+
+        // Simulate asynchronous arrival of data as we always operate on one thread in these tests.
+        peer.expectClose().withError(not(nullValue()));
+        peer.remoteBegin().withRemoteChannel(3).now();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(remoteOpened.get(), "Rmote connection should have occured");
+        assertFalse(remoteSession.get(), "Should not have seen a remote session open.");
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testCapabilitiesArePopulatedAndAccessible() throws Exception {
+        final Symbol clientOfferedSymbol = Symbol.valueOf("clientOfferedCapability");
+        final Symbol clientDesiredSymbol = Symbol.valueOf("clientDesiredCapability");
+        final Symbol serverOfferedSymbol = Symbol.valueOf("serverOfferedCapability");
+        final Symbol serverDesiredSymbol = Symbol.valueOf("serverDesiredCapability");
+
+        Symbol[] clientOfferedCapabilities = new Symbol[] { clientOfferedSymbol };
+        Symbol[] clientDesiredCapabilities = new Symbol[] { clientDesiredSymbol };
+
+        Symbol[] serverOfferedCapabilities = new Symbol[] { serverOfferedSymbol };
+        Symbol[] serverDesiredCapabilities = new Symbol[] { serverDesiredSymbol };
+
+        final AtomicBoolean sessionRemotelyOpened = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().withOfferedCapabilities(new String[] { clientOfferedSymbol.toString() })
+                          .withDesiredCapabilities(new String[] { clientDesiredSymbol.toString() })
+                          .respond()
+                          .withDesiredCapabilities(new String[] { serverDesiredSymbol.toString() })
+                          .withOfferedCapabilities(new String[] { serverOfferedSymbol.toString() });
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+
+        session.setDesiredCapabilities(clientDesiredCapabilities);
+        session.setOfferedCapabilities(clientOfferedCapabilities);
+        session.openHandler(result -> {
+            sessionRemotelyOpened.set(true);
+        });
+        session.open();
+
+        assertTrue(sessionRemotelyOpened.get(), "Session remote opened event did not fire");
+
+        assertArrayEquals(clientOfferedCapabilities, session.getOfferedCapabilities());
+        assertArrayEquals(clientDesiredCapabilities, session.getDesiredCapabilities());
+        assertArrayEquals(serverOfferedCapabilities, session.getRemoteOfferedCapabilities());
+        assertArrayEquals(serverDesiredCapabilities, session.getRemoteDesiredCapabilities());
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testPropertiesArePopulatedAndAccessible() throws Exception {
+        final Symbol clientPropertyName = Symbol.valueOf("ClientPropertyName");
+        final Integer clientPropertyValue = 1234;
+        final Symbol serverPropertyName = Symbol.valueOf("ServerPropertyName");
+        final Integer serverPropertyValue = 5678;
+
+        Map<String, Object> expectedClientProperties = new HashMap<>();
+        expectedClientProperties.put(clientPropertyName.toString(), clientPropertyValue);
+        Map<Symbol, Object> clientProperties = new HashMap<>();
+        clientProperties.put(clientPropertyName, clientPropertyValue);
+
+        Map<String, Object> expectedServerProperties = new HashMap<>();
+        expectedServerProperties.put(serverPropertyName.toString(), serverPropertyValue);
+        Map<Symbol, Object> serverProperties = new HashMap<>();
+        serverProperties.put(serverPropertyName, serverPropertyValue);
+
+        final AtomicBoolean sessionRemotelyOpened = new AtomicBoolean();
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().withProperties(expectedClientProperties)
+                          .respond()
+                          .withProperties(expectedServerProperties);
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+
+        session.setProperties(clientProperties);
+        session.openHandler(result -> {
+            sessionRemotelyOpened.set(true);
+        });
+        session.open();
+
+        assertTrue(sessionRemotelyOpened.get(), "Session remote opened event did not fire");
+
+        assertNotNull(session.getProperties());
+        assertNotNull(session.getRemoteProperties());
+
+        assertEquals(clientPropertyValue, session.getProperties().get(clientPropertyName));
+        assertEquals(serverPropertyValue, session.getRemoteProperties().get(serverPropertyName));
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEmittedSessionIncomingWindowOnFirstFlowNoFrameSizeOrSessionCapacitySet() {
+        doSessionIncomingWindowTestImpl(false, false);
+    }
+
+    @Test
+    public void testEmittedSessionIncomingWindowOnFirstFlowWithFrameSizeButNoSessionCapacitySet() {
+        doSessionIncomingWindowTestImpl(true, false);
+    }
+
+    @Test
+    public void testEmittedSessionIncomingWindowOnFirstFlowWithNoFrameSizeButWithSessionCapacitySet() {
+        doSessionIncomingWindowTestImpl(false, true);
+    }
+
+    @Test
+    public void testEmittedSessionIncomingWindowOnFirstFlowWithFrameSizeAndSessionCapacitySet() {
+        doSessionIncomingWindowTestImpl(true, true);
+    }
+
+    private void doSessionIncomingWindowTestImpl(boolean setFrameSize, boolean setSessionCapacity) {
+        final int TEST_MAX_FRAME_SIZE = 5 * 1024;
+        final int TEST_SESSION_CAPACITY = 100 * 1024;
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final Matcher<?> expectedMaxFrameSize;
+        if (setFrameSize) {
+            expectedMaxFrameSize = new UnsignedIntegerMatcher(TEST_MAX_FRAME_SIZE);
+        } else {
+            expectedMaxFrameSize = new UnsignedIntegerMatcher(ProtonConstants.DEFAULT_MAX_AMQP_FRAME_SIZE);
+        }
+
+        long expectedWindowSize = 2147483647;
+        if (setSessionCapacity && setFrameSize) {
+            expectedWindowSize = TEST_SESSION_CAPACITY / TEST_MAX_FRAME_SIZE;
+        } else if (setSessionCapacity) {
+            expectedWindowSize = TEST_SESSION_CAPACITY / engine.connection().getMaxFrameSize();
+        }
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(expectedMaxFrameSize).respond();
+        peer.expectBegin().withIncomingWindow(expectedWindowSize).respond();
+        peer.expectAttach().respond();
+
+        Connection connection = engine.start();
+        if (setFrameSize) {
+            connection.setMaxFrameSize(TEST_MAX_FRAME_SIZE);
+        }
+        connection.open();
+
+        Session session = connection.session();
+        int sessionCapacity = 0;
+        if (setSessionCapacity) {
+            sessionCapacity = TEST_SESSION_CAPACITY;
+            session.setIncomingCapacity(sessionCapacity);
+        }
+
+        // Open session and verify emitted incoming window
+        session.open();
+
+        if (setSessionCapacity) {
+            assertEquals(sessionCapacity, session.getRemainingIncomingCapacity());
+        } else {
+            assertEquals(Integer.MAX_VALUE, session.getRemainingIncomingCapacity());
+        }
+
+        assertEquals(sessionCapacity, session.getIncomingCapacity(), "Unexpected session capacity");
+
+        // Use a receiver to force more session window observations.
+        Receiver receiver = session.receiver("receiver");
+        receiver.open();
+
+        final AtomicInteger deliveryArrived = new AtomicInteger();
+        final AtomicReference<IncomingDelivery> delivered = new AtomicReference<>();
+        receiver.deliveryReadHandler(delivery -> {
+            deliveryArrived.incrementAndGet();
+            delivered.set(delivery);
+        });
+
+        // Expect that a flow will be emitted and the window should match either default window
+        // size or computed value if max frame size and capacity are set
+        peer.expectFlow().withLinkCredit(1)
+                           .withIncomingWindow(expectedWindowSize);
+        peer.remoteTransfer().withDeliveryId(0)
+                             .withDeliveryTag(new byte[] {0})
+                             .withMore(false)
+                             .withMessageFormat(0)
+                             .withBody().withString("test-message").also().queue();
+
+        receiver.addCredit(1);
+
+        assertEquals(1, deliveryArrived.get(), "Unexpected delivery count");
+        assertNotNull(delivered.get());
+
+        // Flow more credit after receiving a message but not consuming it should result in a decrease in
+        // the incoming window if the capacity and max frame size are configured.
+        if (setSessionCapacity && setFrameSize) {
+            expectedWindowSize = expectedWindowSize - 1;
+            assertTrue(TEST_SESSION_CAPACITY > session.getRemainingIncomingCapacity());
+        }
+
+        peer.expectFlow().withLinkCredit(1)
+                         .withIncomingWindow(expectedWindowSize);
+
+        receiver.addCredit(1);
+
+        // Settle the transfer then flow more credit, verify the emitted incoming window
+        // (it should increase 1 if capacity and frame size set) otherwise remains unchanged.
+        if (setSessionCapacity && setFrameSize) {
+            expectedWindowSize = expectedWindowSize + 1;
+        }
+        peer.expectFlow().withLinkCredit(2).withIncomingWindow(expectedWindowSize);
+
+        // This will consume the bytes and free them from the session window tracking.
+        assertNotNull(delivered.get().readAll());
+
+        receiver.addCredit(1);
+
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        receiver.close();
+        session.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionHandlesDeferredOpenAndBeginResponses() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicInteger sessionOpened = new AtomicInteger();
+        final AtomicInteger sessionClosed = new AtomicInteger();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen();
+        peer.expectBegin();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+        session.openHandler(result -> sessionOpened.incrementAndGet());
+        session.closeHandler(result -> sessionClosed.incrementAndGet());
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+
+        // This should happen after we inject the held open and attach
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Inject held responses to get the ball rolling again
+        peer.remoteOpen().withOfferedCapabilities("ANONYMOUS_REALY").now();
+        peer.respondToLastBegin().now();
+
+        Sender sender = session.sender("sender-1");
+
+        sender.open();
+
+        session.close();
+
+        assertEquals(1, sessionOpened.get(), "Should get one opened event");
+        assertEquals(1, sessionClosed.get(), "Should get one closed event");
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenAndResponseBeginWrittenAndRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, true);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenAndResponseBeginWrittenAndNoRsponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(true, false, false);
+    }
+
+    @Test
+    public void testCloseAfterShutdownDoesNotThrowExceptionOpenNotWritten() throws Exception {
+        testCloseAfterShutdownNoOutputAndNoException(false, false, false);
+    }
+
+    private void testCloseAfterShutdownNoOutputAndNoException(boolean respondToHeader, boolean respondToOpen, boolean respondToBegin) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                if (respondToBegin) {
+                    peer.expectBegin().respond();
+                } else {
+                    peer.expectBegin();
+                }
+            } else {
+                peer.expectOpen();
+                peer.expectBegin();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.open();
+
+        engine.shutdown();
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenAndResponseBeginWrittenAndReponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, true);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenAndResponseBeginWrittenAndNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, true, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenWrittenButNoResponse() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(true, false, false);
+    }
+
+    @Test
+    public void testCloseAfterFailureThrowsEngineStateExceptionOpenNotWritten() throws Exception {
+        testCloseAfterEngineFailedThrowsAndNoOutputWritten(false, false, false);
+    }
+
+    private void testCloseAfterEngineFailedThrowsAndNoOutputWritten(boolean respondToHeader, boolean respondToOpen, boolean respondToBegin) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean engineFailedEvent = new AtomicBoolean();
+
+        if (respondToHeader) {
+            peer.expectAMQPHeader().respondWithAMQPHeader();
+            if (respondToOpen) {
+                peer.expectOpen().respond();
+                if (respondToBegin) {
+                    peer.expectBegin().respond();
+                } else {
+                    peer.expectBegin();
+                }
+                peer.expectClose();
+            } else {
+                peer.expectOpen();
+                peer.expectBegin();
+                peer.expectClose();
+            }
+        } else {
+            peer.expectAMQPHeader();
+        }
+
+        Connection connection = engine.start();
+        connection.open();
+
+        Session session = connection.session();
+        session.engineShutdownHandler(event -> engineFailedEvent.lazySet(true));
+        session.open();
+
+        engine.engineFailed(new IOException());
+
+        try {
+            session.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        try {
+            connection.close();
+            fail("Should throw exception indicating engine is in a failed state.");
+        } catch (EngineFailedException efe) {}
+
+        assertFalse(engineFailedEvent.get(), "Session should not have signalled engine failure");
+
+        engine.shutdown();  // Explicit shutdown now allows local close to complete
+
+        assertTrue(engineFailedEvent.get(), "Session should have signalled engine failure");
+
+        // Should clean up and not throw as we knowingly shutdown engine operations.
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testSessionStopTrackingClosedSenders() throws Exception {
+        doTestSessionTrackingOfLinks(Role.SENDER, true, true, false, true);
+    }
+
+    @Test
+    public void testSessionStopTrackingDetchedSenders() throws Exception {
+        doTestSessionTrackingOfLinks(Role.SENDER, true, true, false, false);
+    }
+
+    @Test
+    public void testSessionStopTrackingClosedSendersRemoteGoesFirst() throws Exception {
+        doTestSessionTrackingOfLinks(Role.SENDER, true, true, true, true);
+    }
+
+    @Test
+    public void testSessionStopTrackingDetachedSendersRemoteGoesFirst() throws Exception {
+        doTestSessionTrackingOfLinks(Role.SENDER, true, true, true, false);
+    }
+
+    @Test
+    public void testSessionTracksRemotelyOpenSenders() throws Exception {
+        doTestSessionTrackingOfLinks(Role.SENDER, true, false, false, false);
+    }
+
+    @Test
+    public void testSessionTracksLocallyOpenSenders() throws Exception {
+        doTestSessionTrackingOfLinks(Role.SENDER, false, true, false, false);
+    }
+
+    @Test
+    public void testSessionStopTrackingClosedReceivers() throws Exception {
+        doTestSessionTrackingOfLinks(Role.RECEIVER, true, true, false, true);
+    }
+
+    @Test
+    public void testSessionStopTrackingDetchedReceivers() throws Exception {
+        doTestSessionTrackingOfLinks(Role.RECEIVER, true, true, false, false);
+    }
+
+    @Test
+    public void testSessionStopTrackingClosedReceiversRemoteGoesFirst() throws Exception {
+        doTestSessionTrackingOfLinks(Role.RECEIVER, true, true, true, true);
+    }
+
+    @Test
+    public void testSessionStopTrackingDetachedReceiversRemoteGoesFirst() throws Exception {
+        doTestSessionTrackingOfLinks(Role.RECEIVER, true, true, true, false);
+    }
+
+    @Test
+    public void testSessionTracksRemotelyOpenReceivers() throws Exception {
+        doTestSessionTrackingOfLinks(Role.RECEIVER, true, false, false, false);
+    }
+
+    @Test
+    public void testSessionTracksLocallyOpenReceivers() throws Exception {
+        doTestSessionTrackingOfLinks(Role.RECEIVER, false, true, false, false);
+    }
+
+    private void doTestSessionTrackingOfLinks(Role role, boolean localDetach, boolean remoteDetach, boolean remoteGoesFirst, boolean close) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+
+        session.open();
+
+        assertTrue(session.senders().isEmpty());
+
+        peer.expectAttach().withRole(role.getValue()).respond();
+
+        final Link<?> link;
+
+        if (role == Role.RECEIVER) {
+            link = session.receiver("test");
+        } else {
+            link = session.sender("test");
+        }
+
+        link.open();
+
+        if (role == Role.RECEIVER) {
+            assertFalse(session.receivers().isEmpty());
+            assertEquals(1, session.receivers().size());
+        } else {
+            assertFalse(session.senders().isEmpty());
+            assertEquals(1, session.senders().size());
+        }
+        assertFalse(session.links().isEmpty());
+        assertEquals(1, session.links().size());
+
+        if (remoteDetach && remoteGoesFirst) {
+            peer.remoteDetach().withClosed(close).now();
+        }
+
+        if (localDetach) {
+            peer.expectDetach().withClosed(close);
+            if (close) {
+                link.close();
+            } else {
+                link.detach();
+            }
+        }
+
+        if (remoteDetach && !remoteGoesFirst) {
+            peer.remoteDetach().withClosed(close).now();
+        }
+
+        if (remoteDetach && localDetach) {
+            assertTrue(session.receivers().isEmpty());
+            assertTrue(session.senders().isEmpty());
+            assertTrue(session.links().isEmpty());
+        } else {
+            if (role == Role.RECEIVER) {
+                assertFalse(session.receivers().isEmpty());
+                assertEquals(1, session.receivers().size());
+            } else {
+                assertFalse(session.senders().isEmpty());
+                assertEquals(1, session.senders().size());
+            }
+            assertFalse(session.links().isEmpty());
+            assertEquals(1, session.links().size());
+        }
+
+        peer.expectEnd().respond();
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testGetSenderFromSessionByName() throws Exception {
+        doTestSessionLinkByName(Role.SENDER);
+    }
+
+    @Test
+    public void testGetReceiverFromSessionByName() throws Exception {
+        doTestSessionLinkByName(Role.RECEIVER);
+    }
+
+    private void doTestSessionLinkByName(Role role) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(role.getValue()).respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+
+        session.open();
+
+        assertTrue(session.senders().isEmpty());
+
+        final Link<?> link;
+
+        if (role == Role.RECEIVER) {
+            link = session.receiver("test");
+        } else {
+            link = session.sender("test");
+        }
+
+        link.open();
+
+        final Link<?> lookup;
+        if (role == Role.RECEIVER) {
+            lookup = session.receiver("test");
+        } else {
+            lookup = session.sender("test");
+        }
+
+        assertSame(link, lookup);
+
+        link.close();
+
+        final Link<?> newLink;
+        if (role == Role.RECEIVER) {
+            newLink = session.receiver("test");
+        } else {
+            newLink = session.sender("test");
+        }
+
+        assertNotSame(link, newLink);
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseOrDetachWithErrorCondition() throws Exception {
+        final String condition = "amqp:session:window-violation";
+        final String description = "something bad happened.";
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectEnd().withError(condition, description).respond();
+        peer.expectClose();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session().open();
+
+        session.setCondition(new ErrorCondition(Symbol.valueOf(condition), description));
+        session.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testSessionNotifiedOfRemoteSenderOpened() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+
+        session.senderOpenHandler(result -> senderRemotelyOpened.set(true));
+        session.open();
+
+        peer.remoteAttach().ofReceiver().withHandle(1)
+                                        .withInitialDeliveryCount(1)
+                                        .withName("remote-sender").now();
+
+        session.close();
+
+        assertTrue(senderRemotelyOpened.get(), "Session should have reported remote sender open");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOpenSenderAndReceiverWithSameLinkNames() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final AtomicBoolean senderRemotelyOpened = new AtomicBoolean();
+        final AtomicBoolean receiverRemotelyOpened = new AtomicBoolean();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().ofSender().withHandle(0).withName("link-name");
+        peer.expectAttach().ofReceiver().withHandle(1).withName("link-name");
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("link-name").open();
+        Receiver receiver = session.receiver("link-name").open();
+
+        sender.openHandler(link -> senderRemotelyOpened.set(true));
+        receiver.openHandler(link -> receiverRemotelyOpened.set(true));
+
+        peer.remoteAttach().ofSender().withHandle(1)
+                                      .withInitialDeliveryCount(1)
+                                      .withName("link-name").now();
+        peer.remoteAttach().ofReceiver().withHandle(0)
+                                        .withInitialDeliveryCount(1)
+                                        .withName("link-name").now();
+
+        assertTrue(sender.isLocallyOpen());
+        assertTrue(sender.isRemotelyOpen());
+        assertTrue(receiver.isLocallyOpen());
+        assertTrue(receiver.isRemotelyOpen());
+
+        session.close();
+
+        assertTrue(senderRemotelyOpened.get(), "Sender should have reported remote sender open");
+        assertTrue(receiverRemotelyOpened.get(), "Receiver should have reported remote sender open");
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testBeginAndEndSessionBeforeRemoteBeginArrives() throws Exception {
+        doTestBeginAndEndSessionBeforeRemoteBeginArrives(false);
+    }
+
+    @Test
+    public void testBeginAndEndSessionBeforeRemoteBeginArrivesForceGC() throws Exception {
+        doTestBeginAndEndSessionBeforeRemoteBeginArrives(true);
+    }
+
+    private void doTestBeginAndEndSessionBeforeRemoteBeginArrives(boolean trgForceGC) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin();
+        peer.expectEnd();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        Session session = connection.session();
+
+        session.open();
+        session.close();
+
+        // Make an "effort" to test the what happens after GC removes references to old sessions
+        // this likely won't work but we at least tried.
+        if (trgForceGC) {
+            System.gc();
+        }
+
+        peer.waitForScriptToComplete();
+        peer.remoteBegin().withRemoteChannel(0).withNextOutgoingId(1).now();
+        peer.remoteEnd().now();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testHalfClosedSessionChannelNotImmediatelyRecycled() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().onChannel(0);
+        peer.expectEnd();
+
+        Connection connection = engine.start();
+
+        connection.open();
+        connection.session().open().close();
+
+        // Channel 0 should be skipped since we are still waiting for the being / end and
+        // we have a free slot that can be used instead.
+        peer.waitForScriptToComplete();
+        peer.expectBegin().onChannel(1).respond();
+        peer.expectEnd().onChannel(1).respond();
+
+        connection.session().open().close();
+
+        // Now channel 1 should reused since it was opened and closed properly
+        peer.waitForScriptToComplete();
+        peer.expectBegin().onChannel(1).respond();
+        peer.expectBegin().onChannel(0).respond();
+        peer.expectEnd().onChannel(0).respond();
+
+        connection.session().open();
+
+        // Close the original session now and its slot should be free to be reused.
+        peer.remoteBegin().withRemoteChannel(0).withNextOutgoingId(1).now();
+        peer.remoteEnd().now();
+
+        connection.session().open().close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testHalfClosedSessionChannelRecycledIfNoOtherAvailableChannels() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withChannelMax(1).respond().withContainerId("driver");
+        peer.expectBegin().onChannel(0);
+        peer.expectEnd().onChannel(0);
+        peer.expectBegin().onChannel(1);
+        peer.expectBegin().onChannel(0);
+
+        Connection connection = engine.start();
+
+        connection.setChannelMax(1); // at most two channels
+        connection.open();
+        connection.session().open().close(); // Ch: 0
+        connection.session().open(); // Ch: 1
+        connection.session().open(); // Ch: 0 (recycled)
+
+        peer.waitForScriptToComplete();
+        // Answer to initial Begin / End of session on Ch: 0
+        peer.remoteBegin().withRemoteChannel(0).onChannel(1).now();
+        peer.remoteEnd().onChannel(1).now();
+        // Answer to second session which should have begun on Ch: 1
+        peer.remoteBegin().withRemoteChannel(1).onChannel(0).now();
+        // Answer to third session which should have begun on Ch: 0 recycled
+        peer.remoteBegin().withRemoteChannel(0).onChannel(1).now();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionEnforcesHandleMaxForLocalSenders() throws Exception {
+        doTestSessionEnforcesHandleMaxForLocalEndpoints(false);
+    }
+
+    @Test
+    public void testSessionEnforcesHandleMaxForLocalReceivers() throws Exception {
+        doTestSessionEnforcesHandleMaxForLocalEndpoints(true);
+    }
+
+    private void doTestSessionEnforcesHandleMaxForLocalEndpoints(boolean receiver) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().withHandleMax(0).respond();
+        peer.expectAttach().respond();
+        peer.expectEnd().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().setHandleMax(0).open();
+
+        assertEquals(0, session.getHandleMax());
+
+        if (receiver) {
+            session.receiver("receiver1").open();
+            try {
+                session.receiver("receiver2").open();
+                fail("Should not allow receiver create on session with one handle maximum");
+            } catch (IllegalStateException ise) {
+                // Expected
+            }
+            try {
+                session.sender("sender1").open();
+                fail("Should not allow additional sender create on session with one handle maximum");
+            } catch (IllegalStateException ise) {
+                // Expected
+            }
+        } else {
+            session.sender("sender1").open();
+            try {
+                session.sender("sender2").open();
+                fail("Should not allow second sender create on session with one handle maximum");
+            } catch (IllegalStateException ise) {
+                // Expected
+            }
+            try {
+                session.receiver("receiver1").open();
+                fail("Should not allow additional receiver create on session with one handle maximum");
+            } catch (IllegalStateException ise) {
+                // Expected
+            }
+        }
+
+        session.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionEnforcesHandleMaxFromRemoteAttachOfSender() throws Exception {
+        doTestSessionEnforcesHandleMaxFromRemoteAttach(true);
+    }
+
+    @Test
+    public void testSessionEnforcesHandleMaxFromRemoteAttachOfReceiver() throws Exception {
+        doTestSessionEnforcesHandleMaxFromRemoteAttach(false);
+    }
+
+    public void doTestSessionEnforcesHandleMaxFromRemoteAttach(boolean sender) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().withHandleMax(0).respond().withHandleMax(42);
+        if (sender) {
+            peer.remoteAttach().ofSender().withHandle(1).withName("link-name").queue();
+        } else {
+            peer.remoteAttach().ofReceiver().withHandle(1).withName("link-name").queue();
+        }
+        peer.expectClose().withError(ConnectionError.FRAMING_ERROR.toString(), "Session handle-max exceeded").respond();
+
+        Connection connection = engine.start().open();
+
+        // Remote should attempt to attach a link and violate local handle max restrictions
+        connection.session().setHandleMax(0).open();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionOutgoingSetEqualToMaxFrameSize() throws Exception {
+        testSessionConfigureOutgoingCapacity(1024, 1024, 1024);
+    }
+
+    @Test
+    public void testSessionOutgoingSetToTwiceMaxFrameSize() throws Exception {
+        testSessionConfigureOutgoingCapacity(1024, 2048, 2048);
+    }
+
+    @Test
+    public void testSessionOutgoingSetToSmallerThanMaxFrameSize() throws Exception {
+        testSessionConfigureOutgoingCapacity(1024, 512, 1024);
+    }
+
+    @Test
+    public void testSessionOutgoingSetToLargerThanMaxFrameSizeAndNotEven() throws Exception {
+        testSessionConfigureOutgoingCapacity(1024, 8199, 8192);
+    }
+
+    @Test
+    public void testSessionOutgoingSetToZeroToDisableOutput() throws Exception {
+        testSessionConfigureOutgoingCapacity(1024, 0, 0);
+    }
+
+    private void testSessionConfigureOutgoingCapacity(int frameSize, int sessionCapacity, int remainingCapacity) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(frameSize).respond();
+        peer.expectBegin().respond();
+
+        Connection connection = engine.start().setMaxFrameSize(frameSize).open();
+        Session session = connection.session().open();
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(Integer.MAX_VALUE, session.getRemainingOutgoingCapacity());
+
+        session.setOutgoingCapacity(sessionCapacity);
+
+        assertEquals(sessionCapacity, session.getOutgoingCapacity());
+        assertEquals(remainingCapacity, session.getRemainingOutgoingCapacity());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSessionNotWritableWhenOutgoingCapacitySetToZeroAlsoReflectsInSenders() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(sender.isSendable());
+        assertEquals(Integer.MAX_VALUE, session.getRemainingOutgoingCapacity());
+
+        session.setOutgoingCapacity(0);
+
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderCannotSendAfterUsingUpOutgoingCapacityLimit() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(2048).open();
+        Sender sender = session.sender("test").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+        peer.expectTransfer().withPayload(payload);
+
+        assertTrue(sender.isSendable());
+        assertEquals(2048, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(2, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderGetsUpdatedOnceSessionOutgoingWindowIsExpandedByWriteCallbacks() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(3072).open();
+        Sender sender = session.sender("test").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+        peer.expectTransfer().withPayload(payload);
+        peer.expectTransfer().withPayload(payload);
+
+        final AtomicInteger creditStateUpdated = new AtomicInteger();
+        sender.creditStateUpdateHandler((self) -> {
+            creditStateUpdated.incrementAndGet();
+        });
+
+        assertTrue(sender.isSendable());
+        assertEquals(3072, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        OutgoingDelivery delivery3 = sender.next();
+        delivery3.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(3, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        assertEquals(1, creditStateUpdated.get());
+        assertTrue(sender.isSendable());
+        assertEquals(3072, session.getRemainingOutgoingCapacity());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSetSameOutgoingWindowAfterBecomingNotWritableDoesNotTriggerWritable() throws Exception {
+        // Should not become writable because two outstanding writes but low water mark remains one frame pending.
+        testSessionOutgoingWindowExpandedAfterItBecomeNotWritable(2048, false);
+    }
+
+    @Test
+    public void testExpandingOutgoingWindowAfterBecomingNotWritableUpdateSenderAsWritableOneFrameBigger() throws Exception {
+        // Should not become writable because two outstanding writes but low water mark remains one frame pending.
+        testSessionOutgoingWindowExpandedAfterItBecomeNotWritable(3072, false);
+    }
+
+    @Test
+    public void testExpandingOutgoingWindowAfterBecomingNotWritableUpdateSenderAsWritableTwoFramesBiffer() throws Exception {
+        // Should become writable since low water mark was one but becomes two and we have only two pending.
+        testSessionOutgoingWindowExpandedAfterItBecomeNotWritable(4096, true);
+    }
+
+    @Test
+    public void testDisableOutgoingWindowingAfterBecomingNotWritableUpdateSenderAsWritable() throws Exception {
+        // Should become pending since we are lifting restrictions
+        testSessionOutgoingWindowExpandedAfterItBecomeNotWritable(-1, true);
+    }
+
+    private void testSessionOutgoingWindowExpandedAfterItBecomeNotWritable(int updatedWindow, boolean becomesWritable) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final int maxFrameSize = 1024;
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(maxFrameSize).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(maxFrameSize).open();
+        Session session = connection.session().setOutgoingCapacity(2048).open();
+        Sender sender = session.sender("test").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+        peer.expectTransfer().withPayload(payload);
+
+        final AtomicInteger creditStateUpdated = new AtomicInteger();
+        sender.creditStateUpdateHandler((self) -> {
+            creditStateUpdated.incrementAndGet();
+        });
+
+        assertTrue(sender.isSendable());
+        assertEquals(2048, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        OutgoingDelivery delivery2 = sender.next();
+        delivery2.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(2, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        session.setOutgoingCapacity(updatedWindow);
+
+        if (becomesWritable) {
+            assertEquals(1, creditStateUpdated.get());
+            assertTrue(sender.isSendable());
+        } else {
+            assertEquals(0, creditStateUpdated.get());
+            assertFalse(sender.isSendable());
+        }
+
+        if (updatedWindow == -1) {
+            assertEquals(Integer.MAX_VALUE, session.getRemainingOutgoingCapacity());
+        } else {
+            assertEquals(updatedWindow - (asyncIOCallbacks.size() * maxFrameSize), session.getRemainingOutgoingCapacity());
+        }
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testMultiplySendersCannotSendAfterUsingUpOutgoingCapacityLimit() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(2048).open();
+        Sender sender1 = session.sender("test1").setDeliveryTagGenerator(generator).open();
+        Sender sender2 = session.sender("test2").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+        peer.expectTransfer().withPayload(payload);
+
+        assertTrue(sender1.isSendable());
+        assertTrue(sender2.isSendable());
+        assertEquals(2048, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach, Attach
+        assertEquals(4, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender1.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        OutgoingDelivery delivery2 = sender2.next();
+        delivery2.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(2, asyncIOCallbacks.size());
+        assertFalse(sender1.isSendable());
+        assertFalse(sender2.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testOnlyOneSenderNotifiedOfNewCapacityIfFirstOneUsesItUp() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(2048).open();
+        Sender sender1 = session.sender("test1").setDeliveryTagGenerator(generator).open();
+        Sender sender2 = session.sender("test2").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+        peer.expectTransfer().withPayload(payload);
+
+        // One of them should write to the high water mark again and stop the other getting called.
+        final AtomicInteger creditStateUpdated = new AtomicInteger();
+        sender1.creditStateUpdateHandler((self) -> {
+            creditStateUpdated.incrementAndGet();
+            OutgoingDelivery delivery = self.next();
+            delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        });
+        sender2.creditStateUpdateHandler((self) -> {
+            creditStateUpdated.incrementAndGet();
+            OutgoingDelivery delivery = self.next();
+            delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        });
+
+        assertTrue(sender1.isSendable());
+        assertTrue(sender2.isSendable());
+        assertEquals(2048, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach, Attach
+        assertEquals(4, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender1.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        OutgoingDelivery delivery2 = sender2.next();
+        delivery2.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        assertEquals(2, asyncIOCallbacks.size());
+        assertFalse(sender1.isSendable());
+        assertFalse(sender2.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        // Free a frame's worth of window which should allow a new write from one sender.
+        asyncIOCallbacks.poll().run();
+
+        assertEquals(2, asyncIOCallbacks.size());
+        assertFalse(sender1.isSendable());
+        assertFalse(sender2.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+        assertEquals(1, creditStateUpdated.get());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReduceOutgoingWindowDoesNotStopSenderIfSomeWindowRemaining() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(4096).open();
+        Sender sender = session.sender("test1").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        assertTrue(sender.isSendable());
+        assertEquals(4096, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertTrue(sender.isSendable());
+        assertEquals(3072, session.getRemainingOutgoingCapacity());
+
+        session.setOutgoingCapacity(2048);
+        assertEquals(1024, session.getRemainingOutgoingCapacity());
+        assertTrue(sender.isSendable());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDisableOutgoingWindowMarksSenderAsNotSendableWhenWriteStillPending() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(4096).open();
+        Sender sender = session.sender("test1").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        assertTrue(sender.isSendable());
+        assertEquals(4096, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertTrue(sender.isSendable());
+        assertEquals(3072, session.getRemainingOutgoingCapacity());
+
+        session.setOutgoingCapacity(0);
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+        assertFalse(sender.isSendable());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testReduceAndThenIncreaseOutgoingWindowRemebersPreviouslyPendingWrites() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(4096).open();
+        Sender sender = session.sender("test1").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        assertTrue(sender.isSendable());
+        assertEquals(4096, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery1 = sender.next();
+        delivery1.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertTrue(sender.isSendable());
+        assertEquals(3072, session.getRemainingOutgoingCapacity());
+
+        session.setOutgoingCapacity(1024);
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+        assertFalse(sender.isSendable());
+        session.setOutgoingCapacity(4096);
+        assertEquals(3072, session.getRemainingOutgoingCapacity());
+        assertTrue(sender.isSendable());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderNotifiedAfterSessionRemoteWindowOpenedAfterLocalCapcityRestored() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().withNextOutgoingId(0).respond().withNextOutgoingId(0);
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).withNextIncomingId(0).withIncomingWindow(1).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(1024).open();
+        Sender sender = session.sender("test1").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        // One of them should write to the high water mark again and stop the other getting called.
+        final AtomicInteger creditStateUpdated = new AtomicInteger();
+        sender.creditStateUpdateHandler((self) -> {
+            creditStateUpdated.incrementAndGet();
+            OutgoingDelivery delivery = self.next();
+            delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+        });
+
+        assertTrue(sender.isSendable());
+        assertEquals(1024, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery = sender.next();
+        delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        // Free a frame's worth of window which shouldn't signal writable as still no remote capacity.
+        asyncIOCallbacks.poll().run();
+
+        assertEquals(0, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(1024, session.getRemainingOutgoingCapacity());
+        assertEquals(0, creditStateUpdated.get());
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+        peer.remoteFlow().withLinkCredit(19).withNextIncomingId(1).withIncomingWindow(1).now();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+        assertEquals(1, creditStateUpdated.get());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSenderNotifiedAfterSessionRemoteWindowOpenedBeforeLocalCapcityRestored() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        Queue<Runnable> asyncIOCallbacks = new ArrayDeque<>();
+        ProtonTestConnector peer = createTestPeer(engine, asyncIOCallbacks);
+
+        final byte[] payload = new byte[] {0, 1, 2, 3, 4};
+        final DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.POOLED.createGenerator();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().withMaxFrameSize(1024).respond();
+        peer.expectBegin().withNextOutgoingId(0).respond().withNextOutgoingId(0);
+        peer.expectAttach().respond();
+        peer.remoteFlow().withLinkCredit(20).withNextIncomingId(0).withIncomingWindow(1).queue();
+
+        Connection connection = engine.start().setMaxFrameSize(1024).open();
+        Session session = connection.session().setOutgoingCapacity(1024).open();
+        Sender sender = session.sender("test1").setDeliveryTagGenerator(generator).open();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        // One of them should write to the high water mark again and stop the other getting called.
+        final AtomicInteger creditStateUpdated = new AtomicInteger();
+        sender.creditStateUpdateHandler((self) -> {
+            creditStateUpdated.incrementAndGet();
+            if (sender.isSendable()) {
+                OutgoingDelivery delivery = self.next();
+                delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+            }
+        });
+
+        assertTrue(sender.isSendable());
+        assertEquals(1024, session.getRemainingOutgoingCapacity());
+
+        // Open, Begin, Attach
+        assertEquals(3, asyncIOCallbacks.size());
+        asyncIOCallbacks.forEach(runner -> runner.run());
+        asyncIOCallbacks.clear();
+
+        OutgoingDelivery delivery = sender.next();
+        delivery.writeBytes(ProtonByteBufferAllocator.DEFAULT.wrap(payload));
+
+        peer.waitForScriptToComplete();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+
+        // Restore session remote incoming capacity but the sender should not send since
+        // there should still be pending I/O work to be signaled.
+        peer.remoteFlow().withLinkCredit(19).withNextIncomingId(1).withIncomingWindow(1).now();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+        assertEquals(1, creditStateUpdated.get());  // For now all flow events create a signal.
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withPayload(payload);
+
+        // Now local outgoing capacity should be opened up.
+        asyncIOCallbacks.poll().run();
+
+        assertEquals(1, asyncIOCallbacks.size());
+        assertFalse(sender.isSendable());
+        assertEquals(0, session.getRemainingOutgoingCapacity());
+        assertEquals(2, creditStateUpdated.get());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testHandleInUseErrorReturnedIfAttachWithAlreadyBoundHandleArrives() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().withHandle(0).respond().withHandle(0);
+        peer.expectAttach().withHandle(1).respond().withHandle(0);
+        peer.expectEnd().withError(SessionError.HANDLE_IN_USE.toString(),  "Attach received with handle that is already in use");
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        session.sender("test1").open();
+        session.sender("test2").open();
+
+        peer.waitForScriptToComplete();
+        peer.expectClose().respond();
+
+        connection.close();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineFailedWhenSessionReceivesDetachForUnknownLink() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.remoteDetach().withHandle(2).onChannel(0).queue();
+        peer.expectClose().withError(notNullValue());
+
+        Connection connection = engine.start().open();
+        connection.session().open();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof ProtocolViolationException);
+    }
+
+    @Test
+    public void testEngineFailedWhenSessionReceivesTransferForUnknownLink() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond().withContainerId("driver");
+        peer.expectBegin().respond();
+        peer.expectAttach().ofReceiver().respond();
+        peer.remoteDetach().queue();
+        peer.remoteTransfer().withHandle(0)
+                             .withDeliveryId(1)
+                             .withDeliveryTag(new byte[] {1})
+                             .onChannel(0)
+                             .queue();
+        peer.expectClose().withError(notNullValue());
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        session.receiver("test").open();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+        assertTrue(failure instanceof ProtocolViolationException);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionControllerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionControllerTest.java
new file mode 100644
index 0000000..1fe4322
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionControllerTest.java
@@ -0,0 +1,1309 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.OutgoingDelivery;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.Transaction;
+import org.apache.qpid.protonj2.engine.TransactionController;
+import org.apache.qpid.protonj2.engine.TransactionState;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.TransactionErrors;
+import org.apache.qpid.protonj2.types.transactions.TransactionalState;
+import org.apache.qpid.protonj2.types.transactions.TxnCapability;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests for AMQP transaction controller abstraction used on client side normally
+ */
+@Timeout(20)
+class ProtonTransactionControllerTest extends ProtonEngineTestSupport {
+
+    private static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(ProtonTransactionControllerTest.class);
+
+    private Symbol[] DEFAULT_OUTCOMES = new Symbol[] { Accepted.DESCRIPTOR_SYMBOL,
+                                                       Rejected.DESCRIPTOR_SYMBOL,
+                                                       Released.DESCRIPTOR_SYMBOL,
+                                                       Modified.DESCRIPTOR_SYMBOL };
+
+    private String[] DEFAULT_OUTCOMES_STRINGS = new String[] { Accepted.DESCRIPTOR_SYMBOL.toString(),
+                                                               Rejected.DESCRIPTOR_SYMBOL.toString(),
+                                                               Released.DESCRIPTOR_SYMBOL.toString(),
+                                                               Modified.DESCRIPTOR_SYMBOL.toString() };
+
+    @Test
+    public void testTransactionControllerDeclaresTransaction() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(1).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        assertSame(session, txnController.getParent());
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean openedWithCoordinatorTarget = new AtomicBoolean();
+        txnController.openHandler(result -> {
+            if (result.getRemoteCoordinator() instanceof Coordinator) {
+                openedWithCoordinatorTarget.set(true);
+            }
+        });
+
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().accept(TXN_ID);
+
+        assertTrue(openedWithCoordinatorTarget.get());
+
+        assertNotNull(txnController.declare());
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertArrayEquals(TXN_ID, declaredTxnId.get());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerSignalsWhenParentSessionClosed() {
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(1).queue();
+        peer.expectDeclare().accept(TXN_ID);
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean openedWithCoordinatorTarget = new AtomicBoolean();
+        txnController.openHandler(result -> {
+            if (result.getRemoteCoordinator() instanceof Coordinator) {
+                openedWithCoordinatorTarget.set(true);
+            }
+        });
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        final AtomicBoolean parentEndpointClosed = new AtomicBoolean();
+        txnController.parentEndpointClosedHandler((controller) -> {
+            parentEndpointClosed.set(true);
+        });
+
+        txnController.open();
+        txnController.declare();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(parentEndpointClosed.get());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerSignalsWhenParentConnectionClosed() {
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(1).queue();
+        peer.expectDeclare().accept(TXN_ID);
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean openedWithCoordinatorTarget = new AtomicBoolean();
+        txnController.openHandler(result -> {
+            if (result.getRemoteCoordinator() instanceof Coordinator) {
+                openedWithCoordinatorTarget.set(true);
+            }
+        });
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        final AtomicBoolean parentEndpointClosed = new AtomicBoolean();
+        txnController.parentEndpointClosedHandler((controller) -> {
+            parentEndpointClosed.set(true);
+        });
+
+        txnController.open();
+        txnController.declare();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(parentEndpointClosed.get());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerSignalsWhenEngineShutdown() {
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(1).queue();
+        peer.expectDeclare().accept(TXN_ID);
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean openedWithCoordinatorTarget = new AtomicBoolean();
+        txnController.openHandler(result -> {
+            if (result.getRemoteCoordinator() instanceof Coordinator) {
+                openedWithCoordinatorTarget.set(true);
+            }
+        });
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        final AtomicBoolean engineShutdown = new AtomicBoolean();
+        txnController.engineShutdownHandler((theEngine) -> {
+            engineShutdown.set(true);
+        });
+
+        txnController.open();
+        txnController.declare();
+
+        engine.shutdown();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(engineShutdown.get());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerDoesNotSignalsWhenParentConnectionClosedIfAlreadyClosed() {
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(1).queue();
+        peer.expectDeclare().accept(TXN_ID);
+        peer.expectDetach().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean openedWithCoordinatorTarget = new AtomicBoolean();
+        txnController.openHandler(result -> {
+            if (result.getRemoteCoordinator() instanceof Coordinator) {
+                openedWithCoordinatorTarget.set(true);
+            }
+        });
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        final AtomicBoolean parentEndpointClosed = new AtomicBoolean();
+        txnController.parentEndpointClosedHandler((controller) -> {
+            parentEndpointClosed.set(true);
+        });
+
+        txnController.open();
+        txnController.declare();
+        txnController.close();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertFalse(parentEndpointClosed.get());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerBeginComiitBeginRollback() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        final byte[] TXN_ID1 = new byte[] { 1, 2, 3, 4 };
+        final byte[] TXN_ID2 = new byte[] { 2, 2, 3, 4 };
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(4).queue();
+        peer.expectDeclare().accept(TXN_ID1);
+        peer.expectDischarge().withFail(false).withTxnId(TXN_ID1).accept();
+        peer.expectDeclare().accept(TXN_ID2);
+        peer.expectDischarge().withFail(true).withTxnId(TXN_ID2).accept();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+        txnController.open();
+
+        assertTrue(txnController.isLocallyOpen());
+        assertTrue(txnController.isRemotelyOpen());
+        assertFalse(txnController.isLocallyClosed());
+        assertFalse(txnController.isRemotelyClosed());
+
+        Transaction<TransactionController> txn1 = txnController.newTransaction();
+        Transaction<TransactionController> txn2 = txnController.newTransaction();
+
+        // Begin / Commit
+        txnController.declare(txn1);
+        txnController.discharge(txn1, false);
+
+        // Begin / Rollback
+        txnController.declare(txn2);
+        txnController.discharge(txn2, true);
+
+        txnController.close();
+
+        assertFalse(txnController.isLocallyOpen());
+        assertFalse(txnController.isRemotelyOpen());
+        assertTrue(txnController.isLocallyClosed());
+        assertTrue(txnController.isRemotelyClosed());
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerDeclareAndDischargeOneTransactionDirect() {
+        doTestTransactionControllerDeclareAndDischargeOneTransaction(false);
+    }
+
+    @Test
+    public void testTransactionControllerDeclareAndDischargeOneTransactionInDirect() {
+        doTestTransactionControllerDeclareAndDischargeOneTransaction(true);
+    }
+
+    private void doTestTransactionControllerDeclareAndDischargeOneTransaction(boolean useNewTransactionAPI) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        final AtomicReference<byte[]> dischargedTxnId = new AtomicReference<>();
+
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+            if (useNewTransactionAPI) {
+                assertEquals(txnController, result.getLinkedResource(TransactionController.class) );
+            } else {
+                assertNull(result.getLinkedResource());
+            }
+        });
+        txnController.dischargedHandler(result -> {
+            dischargedTxnId.set(result.getTxnId().arrayCopy());
+            if (useNewTransactionAPI) {
+                assertEquals(txnController, result.getLinkedResource(TransactionController.class) );
+            } else {
+                assertNull(result.getLinkedResource());
+            }
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().accept(TXN_ID);
+
+        final Transaction<TransactionController> txn;
+        if (useNewTransactionAPI) {
+            txn = txnController.newTransaction();
+            txn.setLinkedResource(txnController);
+            assertEquals(TransactionState.IDLE, txn.getState());
+            txnController.declare(txn);
+        } else {
+            txn = txnController.declare();
+            assertNotNull(txn.getAttachments());
+            assertSame(txn.getAttachments(), txn.getAttachments());
+        }
+
+        assertNotNull(txn);
+
+        peer.waitForScriptToComplete();
+        peer.expectDischarge().withTxnId(TXN_ID).withFail(false).accept();
+
+        assertArrayEquals(TXN_ID, declaredTxnId.get());
+
+        txnController.discharge(txn, false);
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertArrayEquals(TXN_ID, dischargedTxnId.get());
+        assertFalse(txn.isFailed());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionDeclareRejected() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean decalreFailure = new AtomicBoolean();
+        final AtomicReference<Transaction<TransactionController>> failedTxn = new AtomicReference<>();
+
+        final ErrorCondition failureError =
+            new ErrorCondition(AmqpError.INTERNAL_ERROR, "Cannot Declare Transaction at this time");
+
+        txnController.declareFailureHandler(result -> {
+            decalreFailure.set(true);
+            failedTxn.set(result);
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().reject(AmqpError.INTERNAL_ERROR.toString(), "Cannot Declare Transaction at this time");
+
+        final Transaction<TransactionController> txn = txnController.declare();
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(decalreFailure.get());
+        assertSame(txn, failedTxn.get());
+        assertEquals(TransactionState.DECLARE_FAILED, txn.getState());
+        assertEquals(failureError, txn.getCondition());
+        assertTrue(txnController.transactions().isEmpty());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionDischargeRejected() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicBoolean dischargeFailure = new AtomicBoolean();
+        final AtomicReference<Transaction<TransactionController>> failedTxn = new AtomicReference<>();
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.TRANSACTION_TIMEOUT, "Transaction timed out");
+
+        txnController.dischargeFailureHandler(result -> {
+            dischargeFailure.set(true);
+            failedTxn.set(result);
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().accept();
+
+        final Transaction<TransactionController> txn = txnController.declare();
+
+        peer.waitForScriptToComplete();
+        peer.expectDischarge().reject(TransactionErrors.TRANSACTION_TIMEOUT.toString(), "Transaction timed out");
+
+        txnController.discharge(txn, false);
+
+        peer.waitForScriptToComplete();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(dischargeFailure.get());
+        assertSame(txn, failedTxn.get());
+        assertEquals(TransactionState.DISCHARGE_FAILED, txn.getState());
+        assertEquals(failureError, txn.getCondition());
+        assertTrue(txnController.transactions().isEmpty());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotDeclareTransactionFromOneControllerInAnother() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+
+        TransactionController txnController1 = session.coordinator("test-coordinator-1");
+        TransactionController txnController2 = session.coordinator("test-coordinator-2");
+
+        txnController1.setSource(source);
+        txnController1.setCoordinator(coordinator);
+        txnController1.open();
+
+        txnController2.setSource(source);
+        txnController2.setCoordinator(coordinator);
+        txnController2.open();
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(txnController1.hasCapacity());
+        assertTrue(txnController2.hasCapacity());
+
+        final Transaction<TransactionController> txn1 = txnController1.newTransaction();
+        final Transaction<TransactionController> txn2 = txnController2.newTransaction();
+
+        try {
+            txnController1.declare(txn2);
+            fail("Should not be able to declare a transaction with TXN created from another controller");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        try {
+            txnController2.declare(txn1);
+            fail("Should not be able to declare a transaction with TXN created from another controller");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        assertEquals(1, txnController1.transactions().size());
+        assertEquals(1, txnController2.transactions().size());
+
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        txnController1.close();
+        txnController2.close();
+
+        session.close();
+        connection.close();
+
+        // Never discharged so they remain in the controller now
+        assertEquals(1, txnController1.transactions().size());
+        assertEquals(1, txnController2.transactions().size());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotDischargeTransactionFromOneControllerInAnother() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+
+        TransactionController txnController1 = session.coordinator("test-coordinator-1");
+        TransactionController txnController2 = session.coordinator("test-coordinator-2");
+
+        txnController1.setSource(source);
+        txnController1.setCoordinator(coordinator);
+        txnController1.open();
+
+        txnController2.setSource(source);
+        txnController2.setCoordinator(coordinator);
+        txnController2.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().accept();
+        peer.expectDeclare().accept();
+
+        assertTrue(txnController1.hasCapacity());
+        assertTrue(txnController2.hasCapacity());
+
+        final Transaction<TransactionController> txn1 = txnController1.declare();
+        final Transaction<TransactionController> txn2 = txnController2.declare();
+
+        peer.waitForScriptToComplete();
+
+        try {
+            txnController1.discharge(txn2, false);
+            fail("Should not be able to discharge a transaction with TXN created from another controller");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        try {
+            txnController2.discharge(txn1, false);
+            fail("Should not be able to discharge a transaction with TXN created from another controller");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        txnController1.close();
+        txnController2.close();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSendMessageInsideOfTransaction() throws Exception {
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+        final byte [] payloadBuffer = new byte[] {0, 1, 2, 3, 4};
+        final ProtonBuffer payload = ProtonByteBufferAllocator.DEFAULT.wrap(payloadBuffer);
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withRole(Role.SENDER.getValue()).respond();
+        peer.remoteFlow().withLinkCredit(1).queue();
+        peer.expectCoordinatorAttach().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectDeclare().accept(TXN_ID);
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test").open();
+
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+        txnController.open();
+
+        Transaction<TransactionController> txn = txnController.declare();
+
+        peer.waitForScriptToComplete();
+        peer.expectTransfer().withHandle(0)
+                             .withNonNullPayload()
+                             .withState().transactional().withTxnId(TXN_ID).and()
+                             .respond()
+                             .withState().transactional().withTxnId(TXN_ID).withAccepted().and()
+                             .withSettled(true);
+        peer.expectDischarge().withFail(false).withTxnId(TXN_ID).accept();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(sender.isSendable());
+
+        OutgoingDelivery delivery = sender.next();
+
+        delivery.disposition(new TransactionalState().setTxnId(new Binary(TXN_ID)), false);
+        delivery.writeBytes(payload);
+
+        assertTrue(txnController.transactions().contains(txn));
+
+        txnController.discharge(txn, false);
+
+        assertFalse(txnController.transactions().contains(txn));
+
+        assertNotNull(delivery);
+        assertNotNull(delivery.getRemoteState());
+        assertEquals(delivery.getRemoteState().getType(), DeliveryState.DeliveryStateType.Transactional);
+        assertNotNull(delivery.getState());
+        assertEquals(delivery.getState().getType(), DeliveryState.DeliveryStateType.Transactional);
+        assertFalse(delivery.isSettled());
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testCommitTransactionAfterConnectionDropsFollowingTxnDeclared() throws Exception {
+        dischargeTransactionAfterConnectionDropsFollowingTxnDeclared(true);
+    }
+
+    @Test
+    public void testRollbackTransactionAfterConnectionDropsFollowingTxnDeclared() throws Exception {
+        dischargeTransactionAfterConnectionDropsFollowingTxnDeclared(false);
+    }
+
+    public void dischargeTransactionAfterConnectionDropsFollowingTxnDeclared(boolean commit) throws Exception {
+        final byte[] txnId = new byte[] { 0, 1, 2, 3 };
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectCoordinatorAttach().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectDeclare().accept(txnId);
+        peer.dropAfterLastHandler();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+        txnController.open();
+
+        Transaction<TransactionController> txn = txnController.newTransaction();
+
+        txnController.addCapacityAvailableHandler(controller -> {
+            controller.declare(txn);
+        });
+
+        peer.waitForScriptToComplete();
+
+        // The write that are triggered here should fail and throw an exception
+
+        try {
+            if (commit) {
+                txnController.discharge(txn, false);
+            } else {
+                txnController.discharge(txn, true);
+            }
+
+            fail("Should have failed to discharge transaction");
+        } catch (EngineFailedException ex) {
+            // Expected error as a simulated IO disconnect was requested
+            LOG.info("Caught expected EngineFailedException on write of discharge", ex);
+        }
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testTransactionControllerSignalsHandlerWhenCreditAvailableDirect() throws Exception {
+        doTestTransactionControllerSignalsHandlerWhenCreditAvailable(false);
+    }
+
+    @Test
+    public void testTransactionControllerSignalsHandlerWhenCreditAvailableInDirect() throws Exception {
+        doTestTransactionControllerSignalsHandlerWhenCreditAvailable(true);
+    }
+
+    private void doTestTransactionControllerSignalsHandlerWhenCreditAvailable(boolean useNewTransactionAPI) throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        final AtomicReference<byte[]> dischargedTxnId = new AtomicReference<>();
+
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+        txnController.dischargedHandler(result -> {
+            dischargedTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().accept(TXN_ID);
+
+        final AtomicReference<Transaction<TransactionController>> txn = new AtomicReference<>();
+
+        if (useNewTransactionAPI) {
+            txn.set(txnController.newTransaction());
+            try {
+                txnController.declare(txn.get());
+                fail("Should not be able to declare as there is no link credit to do so.");
+            } catch (IllegalStateException ise) {
+            }
+
+            txnController.addCapacityAvailableHandler((controller) -> {
+                txnController.declare(txn.get());
+            });
+        } else {
+            try {
+                txnController.declare();
+                fail("Should not be able to declare as there is no link credit to do so.");
+            } catch (IllegalStateException ise) {
+            }
+
+            txnController.addCapacityAvailableHandler((controller) -> {
+                txn.set(txnController.declare());
+            });
+        }
+
+        peer.remoteFlow().withNextIncomingId(1).withDeliveryCount(0).withLinkCredit(1).now();
+        peer.waitForScriptToComplete();
+        peer.expectDischarge().withTxnId(TXN_ID).withFail(false).accept();
+
+        assertNotNull(txn.get());
+        assertArrayEquals(TXN_ID, declaredTxnId.get());
+
+        try {
+            txnController.discharge(txn.get(), false);
+            fail("Should not be able to discharge as there is no link credit to do so.");
+        } catch (IllegalStateException ise) {
+        }
+
+        txnController.addCapacityAvailableHandler((controller) -> {
+            txnController.discharge(txn.get(), false);
+        });
+
+        peer.remoteFlow().withNextIncomingId(2).withDeliveryCount(1).withLinkCredit(1).now();
+        peer.waitForScriptToComplete();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertArrayEquals(TXN_ID, dischargedTxnId.get());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCapacityAvailableHandlersAreQueuedAndNotifiedWhenCreditGranted() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final byte[] TXN_ID = new byte[] { 1, 2, 3, 4 };
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+        final AtomicReference<byte[]> dischargedTxnId = new AtomicReference<>();
+
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+        txnController.dischargedHandler(result -> {
+            dischargedTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare().accept(TXN_ID);
+
+        final AtomicReference<Transaction<TransactionController>> txn = new AtomicReference<>();
+
+        txnController.addCapacityAvailableHandler((controller) -> {
+            txn.set(txnController.declare());
+        });
+
+        txnController.addCapacityAvailableHandler((controller) -> {
+            txnController.discharge(txn.get(), false);
+        });
+
+        peer.remoteFlow().withNextIncomingId(1).withDeliveryCount(0).withLinkCredit(1).now();
+        peer.waitForScriptToComplete();
+
+        assertTrue(txn.get().isDeclared());
+
+        peer.expectDischarge().withTxnId(TXN_ID).withFail(false).accept();
+
+        assertNotNull(txn.get());
+        assertArrayEquals(TXN_ID, declaredTxnId.get());
+
+        peer.remoteFlow().withNextIncomingId(2).withDeliveryCount(1).withLinkCredit(1).now();
+        peer.waitForScriptToComplete();
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertTrue(txn.get().isDischarged());
+        assertArrayEquals(TXN_ID, dischargedTxnId.get());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionControllerDeclareIsIdempotent() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(3).queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final AtomicReference<byte[]> declaredTxnId = new AtomicReference<>();
+
+        txnController.declaredHandler(result -> {
+            declaredTxnId.set(result.getTxnId().arrayCopy());
+        });
+
+        txnController.open();
+
+        peer.waitForScriptToComplete();
+        peer.expectDeclare();
+
+        final Transaction<TransactionController> txn = txnController.newTransaction();
+
+        txnController.declare(txn);
+
+        assertFalse(txn.isDeclared());  // No response yet
+        assertFalse(txn.isDischarged());
+
+        try {
+            txnController.declare(txn);
+            fail("Should not be able to declare the same transaction a second time.");
+        } catch (IllegalStateException ise) {
+        }
+
+        try {
+            assertEquals(txn.getState(), TransactionState.DECLARING);
+            txnController.discharge(txn, false);
+            fail("Should not be able to discharge a transaction that is not activated by the remote.");
+        } catch (IllegalStateException ise) {
+        }
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionDeclareRejectedWithNoHandlerRegistered() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectDeclare().reject(AmqpError.INTERNAL_ERROR.toString(), "Cannot Declare Transaction at this time");
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        final ErrorCondition failureError =
+            new ErrorCondition(AmqpError.INTERNAL_ERROR, "Cannot Declare Transaction at this time");
+
+        txnController.open();
+
+        final Transaction<TransactionController> txn = txnController.declare();
+
+        assertNotNull(txn.getCondition());
+        assertEquals(TransactionState.DECLARE_FAILED, txn.getState());
+        assertEquals(failureError, txn.getCondition());
+        assertTrue(txnController.transactions().isEmpty());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionDischargeRejectedWithNoHandlerRegistered() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.remoteFlow().withLinkCredit(2).queue();
+        peer.expectDeclare().accept();
+        peer.expectDischarge().reject(TransactionErrors.TRANSACTION_TIMEOUT.toString(), "Transaction timed out");
+        peer.expectDetach().withClosed(true).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        TransactionController txnController = session.coordinator("test-coordinator");
+
+        txnController.setSource(source);
+        txnController.setCoordinator(coordinator);
+
+        txnController.open();
+
+        final Transaction<TransactionController> txn = txnController.declare();
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.TRANSACTION_TIMEOUT, "Transaction timed out");
+
+        txnController.discharge(txn, false);
+
+        assertNotNull(txn.getCondition());
+        assertEquals(TransactionState.DISCHARGE_FAILED, txn.getState());
+        assertEquals(failureError, txn.getCondition());
+        assertTrue(txnController.transactions().isEmpty());
+        assertTrue(txn.isFailed());
+
+        txnController.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionLinkTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionLinkTest.java
new file mode 100644
index 0000000..a79dcfd
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionLinkTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.Receiver;
+import org.apache.qpid.protonj2.engine.Sender;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.apache.qpid.protonj2.types.transactions.TxnCapability;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests for AMQP transaction over normal {@link Sender} and {@link Receiver} links.
+ */
+@Timeout(20)
+public class ProtonTransactionLinkTest extends ProtonEngineTestSupport {
+
+    private Symbol[] DEFAULT_OUTCOMES = new Symbol[] { Accepted.DESCRIPTOR_SYMBOL,
+                                                       Rejected.DESCRIPTOR_SYMBOL,
+                                                       Released.DESCRIPTOR_SYMBOL,
+                                                       Modified.DESCRIPTOR_SYMBOL };
+
+    private String[] DEFAULT_OUTCOMES_STRINGS = new String[] { Accepted.DESCRIPTOR_SYMBOL.toString(),
+                                                               Rejected.DESCRIPTOR_SYMBOL.toString(),
+                                                               Released.DESCRIPTOR_SYMBOL.toString(),
+                                                               Modified.DESCRIPTOR_SYMBOL.toString() };
+
+    @Test
+    public void testCreateDefaultCoordinatorSender() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        Source source = new Source();
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectCoordinatorAttach().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test-coordinator");
+
+        sender.setSource(source);
+        sender.setTarget(coordinator);
+
+        sender.open();
+        sender.detach();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCreateCoordinatorSender() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+
+        Source source = new Source();
+        source.setOutcomes(DEFAULT_OUTCOMES);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.expectAttach().withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().respond();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session().open();
+        Sender sender = session.sender("test-coordinator");
+
+        sender.setSource(source);
+        sender.setTarget(coordinator);
+
+        final AtomicBoolean openedWithCoordinatorTarget = new AtomicBoolean();
+        sender.openHandler(result -> {
+            if (result.getRemoteTarget() instanceof Coordinator) {
+                openedWithCoordinatorTarget.set(true);
+            }
+        });
+
+        sender.open();
+
+        assertTrue(openedWithCoordinatorTarget.get());
+
+        Coordinator remoteCoordinator = sender.getRemoteTarget();
+
+        assertEquals(TxnCapability.LOCAL_TXN, remoteCoordinator.getCapabilities()[0]);
+
+        sender.detach();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testRemoteCoordinatorTriggersSenderCreateWhenManagerHandlerNotSet() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<Receiver> transactionReceiver = new AtomicReference<>();
+        session.receiverOpenHandler(txnReceiver -> {
+            transactionReceiver.set(txnReceiver);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        Receiver manager = transactionReceiver.get();
+
+        assertNotNull(transactionReceiver.get());
+        assertNotNull(transactionReceiver.get().getRemoteTarget());
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.<Coordinator>getRemoteTarget().getCapabilities()[0]);
+
+        manager.setTarget(manager.<Coordinator>getRemoteTarget().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionManagerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionManagerTest.java
new file mode 100644
index 0000000..e21653f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonTransactionManagerTest.java
@@ -0,0 +1,1344 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.Session;
+import org.apache.qpid.protonj2.engine.Transaction;
+import org.apache.qpid.protonj2.engine.TransactionManager;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.AmqpValue;
+import org.apache.qpid.protonj2.types.messaging.Modified;
+import org.apache.qpid.protonj2.types.messaging.Rejected;
+import org.apache.qpid.protonj2.types.messaging.Released;
+import org.apache.qpid.protonj2.types.transactions.TransactionErrors;
+import org.apache.qpid.protonj2.types.transactions.TxnCapability;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.apache.qpid.protonj2.types.transport.Role;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Tests for AMQP transaction manager abstraction used on server side normally
+ */
+@Timeout(20)
+class ProtonTransactionManagerTest extends ProtonEngineTestSupport  {
+
+    private String[] DEFAULT_OUTCOMES_STRINGS = new String[] { Accepted.DESCRIPTOR_SYMBOL.toString(),
+                                                               Rejected.DESCRIPTOR_SYMBOL.toString(),
+                                                               Released.DESCRIPTOR_SYMBOL.toString(),
+                                                               Modified.DESCRIPTOR_SYMBOL.toString() };
+
+    @Test
+    public void testRemoteCoordinatorSenderSignalsTransactionManagerFromSessionWhenEnabled() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+        assertSame(transactionManager.get().getParent(), session);
+
+        TransactionManager manager = transactionManager.get();
+
+        assertFalse(manager.isLocallyClosed());
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertTrue(manager.isLocallyClosed());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCloseRemotelyInitiatedTxnManagerWithErrorCondition() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final ErrorCondition condition = new ErrorCondition(AmqpError.NOT_IMPLEMENTED, "Transactions are not supported");
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource(nullValue())
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectDetach().withError(condition.getCondition().toString(), condition.getDescription()).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+        assertSame(transactionManager.get().getParent(), session);
+
+        TransactionManager manager = transactionManager.get();
+
+        assertFalse(manager.isLocallyClosed());
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(null);
+        manager.open();
+        manager.setCondition(condition);
+        manager.close();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNotNull(manager.getCondition());
+        assertTrue(manager.isLocallyClosed());
+        assertTrue(manager.isRemotelyClosed());
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerAlertedIfParentSessionClosed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+        assertSame(transactionManager.get().getParent(), session);
+
+        final AtomicBoolean parentClosed = new AtomicBoolean();
+
+        TransactionManager manager = transactionManager.get();
+        manager.parentEndpointClosedHandler((txnMgr) -> parentClosed.set(true));
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+
+        session.close();
+
+        assertTrue(parentClosed.get());
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerAlertedIfParentConnectionClosed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectClose().respond();
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+        assertSame(transactionManager.get().getParent(), session);
+
+        final AtomicBoolean parentClosed = new AtomicBoolean();
+
+        TransactionManager manager = transactionManager.get();
+        manager.parentEndpointClosedHandler((txnMgr) -> parentClosed.set(true));
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+
+        assertNotNull(manager.getSource());
+        assertNotNull(manager.getCoordinator());
+
+        manager.open();
+
+        connection.close();
+
+        assertTrue(parentClosed.get());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerAlertedIfEngineShutdown() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+        assertSame(transactionManager.get().getParent(), session);
+
+        final AtomicBoolean engineShutdown = new AtomicBoolean();
+
+        TransactionManager manager = transactionManager.get();
+        manager.engineShutdownHandler((theEngine) -> engineShutdown.set(true));
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+
+        engine.shutdown();
+
+        assertTrue(engineShutdown.get());
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testRemoteCoordinatorSenderSignalsTransactionManagerFromConnectionWhenEnabled() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .withRole(Role.SENDER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        connection.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().withRole(Role.RECEIVER.getValue())
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        TransactionManager manager = transactionManager.get();
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerSignalsTxnDeclarationAndDischargeSucceeds() {
+        doTestTransactionManagerSignalsTxnDeclarationAndDischarge(false);
+    }
+
+    @Test
+    public void testTransactionManagerSignalsTxnDeclarationAndDischargeFailed() {
+        doTestTransactionManagerSignalsTxnDeclarationAndDischarge(true);
+    }
+
+    private void doTestTransactionManagerSignalsTxnDeclarationAndDischarge(boolean txnFailed) {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicBoolean txnRolledBack = new AtomicBoolean();
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(2);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.declareHandler(declared -> {
+            manager.declared(declared, new Binary(TXN_ID));
+        });
+        manager.dischargeHandler(discharged -> {
+            txnRolledBack.set(discharged.getDischargeState().equals(Transaction.DischargeState.ROLLBACK));
+            manager.discharged(discharged);
+        });
+        manager.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID);
+        peer.remoteDischarge().withTxnId(TXN_ID).withFail(txnFailed).withDeliveryId(1).withDeliveryTag(new byte[] {1}).queue();
+        peer.expectDisposition().withState().accepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        if (txnFailed) {
+            assertTrue(txnRolledBack.get());
+        } else {
+            assertFalse(txnRolledBack.get());
+        }
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerSignalsTxnDeclarationFailed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(1);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.TRANSACTION_TIMEOUT, "Transaction timed out");
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.declareHandler(declared -> {
+            manager.declareFailed(declared, failureError);
+        });
+        manager.addCredit(1);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().rejected(failureError.getCondition().toString(), failureError.getDescription());
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerSignalsTxnDischargeFailed() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(2);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.TRANSACTION_TIMEOUT, "Transaction timed out");
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.declareHandler(declared -> {
+            manager.declared(declared, new Binary(TXN_ID));
+        });
+        manager.dischargeHandler(discharged -> {
+            manager.dischargeFailed(discharged, failureError);
+        });
+        manager.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID);
+        peer.remoteDischarge().withTxnId(TXN_ID).withFail(false).withDeliveryId(1).withDeliveryTag(new byte[] {1}).queue();
+        peer.expectDisposition().withState().rejected(failureError.getCondition().toString(), failureError.getDescription());
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertEquals(0, manager.getCredit());
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testManagerChecksDeclaredArgumentsForSomeCorrectness() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(1);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+        final AtomicReference<Transaction<TransactionManager>> txn = new AtomicReference<>();
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.declareHandler(declared -> {
+            txn.set(declared);
+        });
+        manager.addCredit(1);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID).withAccepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertNotNull(txn.get());
+
+        assertThrows(IllegalArgumentException.class, () -> manager.declared(txn.get(), (Binary) null));
+        assertThrows(IllegalArgumentException.class, () -> manager.declared(txn.get(), new Binary(new byte[0])));
+        assertThrows(IllegalArgumentException.class, () -> manager.declared(txn.get(), new Binary((ProtonBuffer) null)));
+        assertThrows(NullPointerException.class, () -> manager.declared(txn.get(), (byte[]) null));
+        assertThrows(IllegalArgumentException.class, () -> manager.declared(txn.get(), new byte[0]));
+
+        manager.declared(txn.get(), new Binary(TXN_ID));
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testManagerIgnoresAbortedTransfers() throws Exception {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(1);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+        final AtomicReference<Transaction<TransactionManager>> txn = new AtomicReference<>();
+        final AtomicInteger declareCounter = new AtomicInteger();
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.declareHandler(declared -> {
+            declareCounter.incrementAndGet();
+            txn.set(declared);
+        });
+        manager.addCredit(1);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID).withAccepted();
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames aborting first attempt and then getting it right.
+        peer.remoteDeclare().withMore(true).withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+        peer.remoteTransfer().withDeliveryId(0).withAborted(true).now();
+        peer.remoteDeclare().withDeliveryId(1).withDeliveryTag(new byte[] {1}).now();
+
+        assertNotNull(txn.get());
+        assertEquals(1, declareCounter.get());
+
+        manager.declared(txn.get(), new Binary(TXN_ID));
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSignalDeclaredFromAnotherTransactionManager() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link-1")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+        peer.remoteAttach().withName("TXN-Link-2")
+                           .withHandle(1)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager1 = new AtomicReference<>();
+        final AtomicReference<TransactionManager> transactionManager2 = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            if (transactionManager1.get() == null) {
+                transactionManager1.set(manager);
+            } else {
+                transactionManager2.set(manager);
+            }
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(0)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(0).withLinkCredit(2);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(1)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(1).withLinkCredit(2);
+
+        assertNotNull(transactionManager1.get());
+        assertNotNull(transactionManager1.get().getRemoteCoordinator());
+        assertNotNull(transactionManager2.get());
+        assertNotNull(transactionManager2.get().getRemoteCoordinator());
+
+        final TransactionManager manager1 = transactionManager1.get();
+        final TransactionManager manager2 = transactionManager2.get();
+
+        final AtomicReference<Transaction<TransactionManager>> txn = new AtomicReference<>();
+
+        manager1.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager1.setSource(manager1.getRemoteSource().copy());
+        manager1.open();
+        manager1.declareHandler(declared -> {
+            txn.set(declared);
+        });
+        manager1.dischargeHandler(discharged -> {
+            manager1.discharged(discharged);
+        });
+        manager1.addCredit(2);
+
+        // Put number two into a valid state as well.
+        manager2.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager2.setSource(manager1.getRemoteSource().copy());
+        manager2.open();
+        manager2.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID);
+        peer.remoteDischarge().withHandle(0).withTxnId(TXN_ID).withFail(false).withDeliveryId(1).withDeliveryTag(new byte[] {1}).queue();
+        peer.expectDisposition().withState().accepted();
+        peer.expectDetach().withHandle(0).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withHandle(0).withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertNotNull(txn.get());
+
+        assertThrows(IllegalArgumentException.class, () -> manager2.declared(txn.get(), new Binary(TXN_ID)));
+
+        manager1.declared(txn.get(), new Binary(TXN_ID));
+
+        manager1.close();
+        manager2.close();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSignalDeclareFailedFromAnotherTransactionManager() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link-1")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+        peer.remoteAttach().withName("TXN-Link-2")
+                           .withHandle(1)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager1 = new AtomicReference<>();
+        final AtomicReference<TransactionManager> transactionManager2 = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            if (transactionManager1.get() == null) {
+                transactionManager1.set(manager);
+            } else {
+                transactionManager2.set(manager);
+            }
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(0)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(0).withLinkCredit(2);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(1)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(1).withLinkCredit(2);
+
+        assertNotNull(transactionManager1.get());
+        assertNotNull(transactionManager1.get().getRemoteCoordinator());
+        assertNotNull(transactionManager2.get());
+        assertNotNull(transactionManager2.get().getRemoteCoordinator());
+
+        final TransactionManager manager1 = transactionManager1.get();
+        final TransactionManager manager2 = transactionManager2.get();
+
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.UNKNOWN_ID, "Transaction unknown for some reason");
+
+        final AtomicReference<Transaction<TransactionManager>> txn = new AtomicReference<>();
+
+        manager1.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager1.setSource(manager1.getRemoteSource().copy());
+        manager1.open();
+        manager1.declareHandler(declared -> {
+            txn.set(declared);
+        });
+        manager1.addCredit(2);
+
+        // Put number two into a valid state as well.
+        manager2.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager2.setSource(manager1.getRemoteSource().copy());
+        manager2.open();
+        manager2.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().rejected(failureError.getCondition().toString(), failureError.getDescription());
+        peer.expectDetach().withHandle(0).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withHandle(0).withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertNotNull(txn.get());
+
+        assertThrows(IllegalArgumentException.class, () -> manager2.declareFailed(txn.get(), failureError));
+
+        manager1.declareFailed(txn.get(), failureError);
+
+        manager1.close();
+        manager2.close();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSignalDischargedFromAnotherTransactionManager() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link-1")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+        peer.remoteAttach().withName("TXN-Link-2")
+                           .withHandle(1)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager1 = new AtomicReference<>();
+        final AtomicReference<TransactionManager> transactionManager2 = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            if (transactionManager1.get() == null) {
+                transactionManager1.set(manager);
+            } else {
+                transactionManager2.set(manager);
+            }
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(0)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(0).withLinkCredit(2);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(1)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(1).withLinkCredit(2);
+
+        assertNotNull(transactionManager1.get());
+        assertNotNull(transactionManager1.get().getRemoteCoordinator());
+        assertNotNull(transactionManager2.get());
+        assertNotNull(transactionManager2.get().getRemoteCoordinator());
+
+        final TransactionManager manager1 = transactionManager1.get();
+        final TransactionManager manager2 = transactionManager2.get();
+
+        final AtomicReference<Transaction<TransactionManager>> txn = new AtomicReference<>();
+
+        manager1.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager1.setSource(manager1.getRemoteSource().copy());
+        manager1.open();
+        manager1.declareHandler(declared -> {
+            txn.set(declared);
+            manager1.declared(declared, TXN_ID);
+        });
+        manager1.addCredit(2);
+
+        // Put number two into a valid state as well.
+        manager2.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager2.setSource(manager1.getRemoteSource().copy());
+        manager2.open();
+        manager2.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID);
+        peer.remoteDischarge().withHandle(0).withTxnId(TXN_ID).withFail(false).withDeliveryId(1).withDeliveryTag(new byte[] {1}).queue();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withHandle(0).withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertNotNull(txn.get());
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().accepted();
+        peer.expectDetach().withHandle(0).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertThrows(IllegalArgumentException.class, () -> manager2.discharged(txn.get()));
+
+        manager1.discharged(txn.get());
+
+        manager1.close();
+        manager2.close();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testCannotSignalDischargeFailedFromAnotherTransactionManager() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link-1")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+        peer.remoteAttach().withName("TXN-Link-2")
+                           .withHandle(1)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager1 = new AtomicReference<>();
+        final AtomicReference<TransactionManager> transactionManager2 = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            if (transactionManager1.get() == null) {
+                transactionManager1.set(manager);
+            } else {
+                transactionManager2.set(manager);
+            }
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(0)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(0).withLinkCredit(2);
+        peer.expectAttach().ofReceiver()
+                           .withHandle(1)
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withHandle(1).withLinkCredit(2);
+
+        assertNotNull(transactionManager1.get());
+        assertNotNull(transactionManager1.get().getRemoteCoordinator());
+        assertNotNull(transactionManager2.get());
+        assertNotNull(transactionManager2.get().getRemoteCoordinator());
+
+        final TransactionManager manager1 = transactionManager1.get();
+        final TransactionManager manager2 = transactionManager2.get();
+
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.UNKNOWN_ID, "Transaction unknown for some reason");
+
+        final AtomicReference<Transaction<TransactionManager>> txn = new AtomicReference<>();
+
+        manager1.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager1.setSource(manager1.getRemoteSource().copy());
+        manager1.open();
+        manager1.declareHandler(declared -> {
+            txn.set(declared);
+            manager1.declared(declared, TXN_ID);
+        });
+        manager1.addCredit(2);
+
+        // Put number two into a valid state as well.
+        manager2.setCoordinator(manager1.getRemoteCoordinator().copy());
+        manager2.setSource(manager1.getRemoteSource().copy());
+        manager2.open();
+        manager2.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID);
+        peer.remoteDischarge().withHandle(0).withTxnId(TXN_ID).withFail(false).withDeliveryId(1).withDeliveryTag(new byte[] {1}).queue();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withHandle(0).withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertNotNull(txn.get());
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().rejected(failureError.getCondition().toString(), failureError.getDescription());
+        peer.expectDetach().withHandle(0).respond();
+        peer.expectDetach().withHandle(1).respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        assertThrows(IllegalArgumentException.class, () -> manager2.dischargeFailed(txn.get(), failureError));
+
+        manager1.dischargeFailed(txn.get(), failureError);
+
+        manager1.close();
+        manager2.close();
+
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testTransactionManagerRejectsAttemptedDischargeOfUnkownTxnId() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] TXN_ID = new byte[] {0, 1, 2, 3};
+        final byte[] TXN_ID_UNKNOWN= new byte[] {3, 2, 1, 0};
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(2);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+        final ErrorCondition failureError =
+            new ErrorCondition(TransactionErrors.UNKNOWN_ID, "Transaction Manager is not tracking the given transaction ID.");
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.declareHandler(declared -> {
+            manager.declared(declared, new Binary(TXN_ID));
+        });
+        manager.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectDisposition().withState().transactional().withTxnId(TXN_ID);
+        peer.remoteDischarge().withTxnId(TXN_ID_UNKNOWN).withFail(false).withDeliveryId(1).withDeliveryTag(new byte[] {1}).queue();
+        peer.expectDisposition().withState().rejected(failureError.getCondition().toString(), failureError.getDescription());
+        peer.expectDetach().respond();
+        peer.expectEnd().respond();
+        peer.expectClose().respond();
+
+        // Starts the flow of Transaction frames
+        peer.remoteDeclare().withDeliveryId(0).withDeliveryTag(new byte[] {0}).now();
+
+        assertEquals(0, manager.getCredit());
+
+        manager.close();
+        session.close();
+        connection.close();
+
+        peer.waitForScriptToComplete();
+        assertNull(failure);
+    }
+
+    @Test
+    public void testEngineFailedIfNonTxnRelatedTransferArrivesAtCoordinator() {
+        Engine engine = EngineFactory.PROTON.createNonSaslEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        final byte[] payload = createEncodedMessage(new AmqpValue<>("test"));
+
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectBegin().respond();
+        peer.remoteAttach().withName("TXN-Link")
+                           .withHandle(0)
+                           .ofSender()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withInitialDeliveryCount(0)
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString()).and().queue();
+
+        Connection connection = engine.start().open();
+        Session session = connection.session();
+
+        final AtomicReference<TransactionManager> transactionManager = new AtomicReference<>();
+        session.transactionManagerOpenHandler(manager -> {
+            transactionManager.set(manager);
+        });
+
+        session.open();
+
+        peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        peer.expectAttach().ofReceiver()
+                           .withSource().withOutcomes(DEFAULT_OUTCOMES_STRINGS).and()
+                           .withCoordinator().withCapabilities(TxnCapability.LOCAL_TXN.toString());
+        peer.expectFlow().withLinkCredit(2);
+
+        assertNotNull(transactionManager.get());
+        assertNotNull(transactionManager.get().getRemoteCoordinator());
+
+        final TransactionManager manager = transactionManager.get();
+
+        assertEquals(TxnCapability.LOCAL_TXN, manager.getRemoteCoordinator().getCapabilities()[0]);
+
+        manager.setCoordinator(manager.getRemoteCoordinator().copy());
+        manager.setSource(manager.getRemoteSource().copy());
+        manager.open();
+        manager.addCredit(2);
+
+        peer.waitForScriptToComplete();
+        peer.expectClose().withError(notNullValue());
+        // Send the invalid Transfer to trigger engine shutdown
+        peer.remoteTransfer().withDeliveryTag(new byte[] {0})
+                             .withPayload(payload)
+                             .withMore(false)
+                             .now();
+
+        assertTrue(engine.isFailed());
+
+        // The transfer write should trigger an error back into the peer which we can ignore.
+        peer.waitForScriptToCompleteIgnoreErrors();
+        assertNotNull(failure);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonUuidTagGeneratorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonUuidTagGeneratorTest.java
new file mode 100644
index 0000000..bd69221
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/ProtonUuidTagGeneratorTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.buffer.ProtonByteUtils;
+import org.apache.qpid.protonj2.engine.DeliveryTagGenerator;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.junit.jupiter.api.Test;
+
+public class ProtonUuidTagGeneratorTest {
+
+    @Test
+    public void testCreateTagGenerator() {
+        DeliveryTagGenerator generator = ProtonDeliveryTagGenerator.BUILTIN.UUID.createGenerator();
+        assertTrue(generator instanceof ProtonUuidTagGenerator);
+        assertNotNull(generator.toString());
+    }
+
+    @Test
+    public void testCreateTag() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+        assertNotNull(generator.nextTag());
+        DeliveryTag next = generator.nextTag();
+        next.release();
+        assertNotSame(next, generator.nextTag());
+    }
+
+    @Test
+    public void testCopyTag() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+        DeliveryTag next = generator.nextTag();
+        DeliveryTag copy = next.copy();
+
+        assertNotSame(next, copy);
+        assertEquals(next, copy);
+    }
+
+    @Test
+    public void testTagCreatedHasExpectedUnderlying() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+
+        DeliveryTag tag = generator.nextTag();
+
+        assertEquals(16, tag.tagLength());
+
+        byte[] tagBuffer = tag.tagBuffer().getArray();
+
+        long msBytes = ProtonByteUtils.readLong(tagBuffer, 0);
+        long lsBytes = ProtonByteUtils.readLong(tagBuffer, 8);
+
+        UUID uuid = new UUID(msBytes, lsBytes);
+
+        assertNotNull(uuid);
+        assertEquals(tag.hashCode(), uuid.hashCode());
+        assertEquals(uuid.toString(), tag.toString());
+    }
+
+    @Test
+    public void testTagCreatedHasExpectedUnderlyingBuffer() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+
+        DeliveryTag tag = generator.nextTag();
+
+        assertEquals(16, tag.tagLength());
+
+        byte[] tagBuffer = tag.tagBuffer().getArray();
+
+        long msBytes = ProtonByteUtils.readLong(tagBuffer, 0);
+        long lsBytes = ProtonByteUtils.readLong(tagBuffer, 8);
+
+        UUID uuid = new UUID(msBytes, lsBytes);
+
+        assertNotNull(uuid);
+        assertEquals(tag.hashCode(), uuid.hashCode());
+        assertEquals(uuid.toString(), tag.toString());
+    }
+
+    @Test
+    public void testCreateMatchingUUIDFromWrittenBuffer() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.allocate(16, 16);
+
+        DeliveryTag tag = generator.nextTag();
+
+        tag.writeTo(buffer);
+
+        assertEquals(16, buffer.getReadableBytes());
+
+        byte[] tagBuffer = tag.tagBuffer().getArray();
+
+        long msBytes = ProtonByteUtils.readLong(tagBuffer, 0);
+        long lsBytes = ProtonByteUtils.readLong(tagBuffer, 8);
+
+        UUID uuid = new UUID(msBytes, lsBytes);
+
+        assertNotNull(uuid);
+        assertEquals(tag.hashCode(), uuid.hashCode());
+        assertEquals(uuid.toString(), tag.toString());
+    }
+
+    @Test
+    public void testTagEquals() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+
+        DeliveryTag tag1 = generator.nextTag();
+        DeliveryTag tag2 = generator.nextTag();
+        DeliveryTag tag3 = generator.nextTag();
+
+        assertEquals(tag1, tag1);
+        assertNotEquals(tag1, tag2);
+        assertNotEquals(tag2, tag3);
+        assertNotEquals(tag1, tag3);
+
+        assertNotEquals(null, tag1);
+        assertNotEquals(tag1, null);
+        assertNotEquals("something", tag1);
+        assertNotEquals(tag2, "something");
+    }
+
+    @Test
+    public void testCreateTagsAreNotEqual() {
+        ProtonUuidTagGenerator generator = new ProtonUuidTagGenerator();
+
+        DeliveryTag tag1 = generator.nextTag();
+        DeliveryTag tag2 = generator.nextTag();
+
+        assertNotSame(tag1, tag2);
+        assertNotEquals(tag1, tag2);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java
new file mode 100644
index 0000000..79f082c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.security.Principal;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.engine.Connection;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngineTestSupport;
+import org.apache.qpid.protonj2.engine.sasl.client.SaslAuthenticator;
+import org.apache.qpid.protonj2.engine.sasl.client.SaslCredentialsProvider;
+import org.apache.qpid.protonj2.test.driver.ProtonTestConnector;
+import org.apache.qpid.protonj2.types.security.SaslCode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Test proton engine from the perspective of a SASL client
+ */
+@Timeout(20)
+public class ProtonSaslClientTest extends ProtonEngineTestSupport {
+
+    @Test
+    public void testSaslAnonymousConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectSASLAnonymousConnect();
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator(null, null));
+
+        Connection connection = engine.start().open();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDriverThrowsIfServerStateRequestedAfterClientStateActivated() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectSASLAnonymousConnect();
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator(null, null));
+
+        assertThrows(IllegalStateException.class, () -> engine.saslDriver().server());
+
+        Connection connection = engine.start().open();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSaslAnonymousConnectionWhenPlainAlsoOfferedButNoCredentialsGiven() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectSASLHeader().respondWithSASLPHeader();
+        peer.remoteSaslMechanisms().withMechanisms("PLAIN", "ANONYMOUS").queue();
+        peer.expectSaslInit().withMechanism("ANONYMOUS");
+        peer.remoteSaslOutcome().withCode(SaslCode.OK.byteValue()).queue();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator(null, null));
+
+        Connection connection = engine.start().open();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSaslPlainConnection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Expect a PLAIN connection
+        String user = "user";
+        String pass = "qwerty123456";
+
+        peer.expectSASLPlainConnect(user, pass);
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator(user, pass));
+
+        Connection connection = engine.start().open();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSaslPlainConnectionWhenUnknownMechanismsOfferedBeforeIt() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Expect a PLAIN connection
+        String user = "user";
+        String pass = "qwerty123456";
+
+        peer.expectSASLHeader().respondWithSASLPHeader();
+        peer.remoteSaslMechanisms().withMechanisms("UNKNOWN", "PLAIN", "ANONYMOUS").queue();
+        peer.expectSaslInit().withMechanism("PLAIN").withInitialResponse(peer.saslPlainInitialResponse(user, pass));
+        peer.remoteSaslOutcome().withCode(SaslCode.OK.byteValue()).queue();
+        peer.expectAMQPHeader().respondWithAMQPHeader();
+
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator(user, pass));
+
+        Connection connection = engine.start().open();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testDefaultClientSaslMismatchBetweenClientAndServer() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectSASLHeader().respondWithSASLPHeader();
+        peer.remoteSaslMechanisms().withMechanisms("PLAIN").queue();
+
+        // Default client only know about ANONYMOUS
+        engine.saslDriver().client();
+
+        Connection connection = engine.start().open();
+
+        try {
+            connection.close();
+            fail("Engine should have failed");
+        } catch (EngineFailedException efe) {
+            // Expected as engine failed but was not shutdown
+        }
+
+        peer.waitForScriptToComplete();
+
+        assertTrue(engine.isFailed());
+        assertNotNull(failure);
+    }
+
+    @Test
+    public void testSaslXOauth2Connection() throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        // Expect a XOAUTH2 connection
+        String user = "user";
+        String pass = "eyB1c2VyPSJ1c2VyIiB9";
+
+        peer.expectSaslXOauth2Connect(user, pass);
+        peer.expectOpen().respond();
+        peer.expectClose().respond();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator(user, pass));
+
+        Connection connection = engine.start().open();
+
+        connection.close();
+
+        peer.waitForScriptToComplete();
+
+        assertNull(failure);
+    }
+
+    @Test
+    public void testSaslFailureCodesFailEngine() throws Exception {
+        doSaslFailureCodesTestImpl(SaslCode.AUTH);
+        doSaslFailureCodesTestImpl(SaslCode.SYS);
+        doSaslFailureCodesTestImpl(SaslCode.SYS_PERM);
+        doSaslFailureCodesTestImpl(SaslCode.SYS_TEMP);
+    }
+
+    private void doSaslFailureCodesTestImpl(SaslCode saslFailureCode) throws Exception {
+        Engine engine = EngineFactory.PROTON.createEngine();
+        engine.errorHandler(result -> failure = result.failureCause());
+        ProtonTestConnector peer = createTestPeer(engine);
+
+        peer.expectSASLHeader().respondWithSASLPHeader();
+        peer.remoteSaslMechanisms().withMechanisms("PLAIN", "ANONYMOUS").queue();
+        peer.expectSaslInit().withMechanism("PLAIN");
+        peer.remoteSaslOutcome().withCode(saslFailureCode.byteValue()).queue();
+
+        engine.saslDriver().client().setListener(createSaslPlainAuthenticator("user", "pass"));
+
+        engine.start().open();
+
+        peer.waitForScriptToComplete();
+
+        assertNotNull(failure);
+        assertFalse(engine.isShutdown());
+        assertTrue(engine.isFailed());
+        assertEquals(failure, engine.failureCause());
+        assertTrue(failure instanceof SaslException);
+    }
+
+    private SaslAuthenticator createSaslPlainAuthenticator(String user, String password) {
+        SaslCredentialsProvider credentials = new SaslCredentialsProvider() {
+
+            @Override
+            public String vhost() {
+                return null;
+            }
+
+            @Override
+            public String username() {
+                return user;
+            }
+
+            @Override
+            public String password() {
+                return password;
+            }
+
+            @Override
+            public Principal localPrincipal() {
+                return null;
+            }
+        };
+
+        return new SaslAuthenticator(credentials);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslHandlerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslHandlerTest.java
new file mode 100644
index 0000000..40b4a80
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslHandlerTest.java
@@ -0,0 +1,673 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.impl.sasl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.engine.AMQPPerformativeEnvelopePool;
+import org.apache.qpid.protonj2.engine.Engine;
+import org.apache.qpid.protonj2.engine.EngineFactory;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver;
+import org.apache.qpid.protonj2.engine.EngineSaslDriver.SaslState;
+import org.apache.qpid.protonj2.engine.EngineState;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.PerformativeEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+import org.apache.qpid.protonj2.engine.exceptions.EngineFailedException;
+import org.apache.qpid.protonj2.engine.exceptions.EngineStartedException;
+import org.apache.qpid.protonj2.engine.exceptions.ProtocolViolationException;
+import org.apache.qpid.protonj2.engine.impl.ProtonConstants;
+import org.apache.qpid.protonj2.engine.impl.ProtonEngine;
+import org.apache.qpid.protonj2.engine.sasl.SaslClientContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslOutcome;
+import org.apache.qpid.protonj2.engine.sasl.SaslServerContext;
+import org.apache.qpid.protonj2.engine.sasl.SaslServerListener;
+import org.apache.qpid.protonj2.engine.util.FrameReadSinkTransportHandler;
+import org.apache.qpid.protonj2.engine.util.FrameRecordingTransportHandler;
+import org.apache.qpid.protonj2.engine.util.FrameWriteSinkTransportHandler;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslCode;
+import org.apache.qpid.protonj2.types.security.SaslInit;
+import org.apache.qpid.protonj2.types.security.SaslMechanisms;
+import org.apache.qpid.protonj2.types.security.SaslPerformative;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader;
+import org.apache.qpid.protonj2.types.transport.Open;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests SASL Handling by the SaslHandler TransportHandler class.
+ */
+public class ProtonSaslHandlerTest {
+
+    private FrameRecordingTransportHandler testHandler;
+
+    @BeforeEach
+    public void setUp() {
+        testHandler = new FrameRecordingTransportHandler();
+    }
+
+    @Test
+    public void testCanRemoveSaslClientHandlerBeforeEngineStarted() {
+        doTestCanRemoveSaslHandlerBeforeEngineStarted(false);
+    }
+
+    @Test
+    public void testCanRemoveSaslServerHandlerBeforeEngineStarted() {
+        doTestCanRemoveSaslHandlerBeforeEngineStarted(true);
+    }
+
+    private void doTestCanRemoveSaslHandlerBeforeEngineStarted(boolean server) {
+        final Engine engine;
+
+        if (server) {
+            engine = createSaslServerEngine();
+        } else {
+            engine = createSaslClientEngine();
+        }
+
+        assertNotNull(engine.pipeline().find(ProtonConstants.SASL_PERFORMATIVE_HANDLER));
+
+        engine.pipeline().remove(ProtonConstants.SASL_PERFORMATIVE_HANDLER);
+
+        assertNull(engine.pipeline().find(ProtonConstants.SASL_PERFORMATIVE_HANDLER));
+    }
+
+    @Test
+    public void testCannotInitiateSaslClientHandlerAfterEngineShutdown() {
+        doTestCannotInitiateSaslHandlerAfterEngineShutdown(false);
+    }
+
+    @Test
+    public void testCannotInitiateSaslServerHandlerAfterEngineShutdown() {
+        doTestCannotInitiateSaslHandlerAfterEngineShutdown(true);
+    }
+
+    private void doTestCannotInitiateSaslHandlerAfterEngineShutdown(boolean server) {
+        final Engine engine = createSaslCapableEngine();
+
+        engine.shutdown();
+
+        if (server) {
+            assertThrows(IllegalStateException.class, ()-> engine.saslDriver().server());
+        } else {
+            assertThrows(IllegalStateException.class, ()-> engine.saslDriver().client());
+        }
+    }
+
+    // TODO: Prevent removal from the pipeline.
+
+    @Disabled("Need a mechanism to ensure handler is locked into pipeline")
+    @Test
+    public void testCannotRemoveSaslClientHandlerAfterEngineStarted() {
+        doTestCanRemoveSaslHandlerAfterEngineStarted(false);
+    }
+
+    @Disabled("Need a mechanism to ensure handler is locked into pipeline")
+    @Test
+    public void testCannotRemoveSaslServerHandlerAfterEngineStarted() {
+        doTestCanRemoveSaslHandlerAfterEngineStarted(true);
+    }
+
+    private void doTestCanRemoveSaslHandlerAfterEngineStarted(boolean server) {
+        final Engine engine;
+
+        if (server) {
+            engine = createSaslServerEngine();
+        } else {
+            engine = createSaslClientEngine();
+        }
+
+        assertNotNull(engine.pipeline().find(ProtonConstants.SASL_PERFORMATIVE_HANDLER));
+
+        engine.start();
+        engine.pipeline().remove(ProtonConstants.SASL_PERFORMATIVE_HANDLER);
+
+        assertNotNull(engine.pipeline().find(ProtonConstants.SASL_PERFORMATIVE_HANDLER));
+    }
+
+    @Test
+    public void testCannotSaslDriverChangeMaxFrameSizeAfterSASLAuthBegins() {
+        final Engine engine = createSaslServerEngine();
+
+        engine.start();
+        engine.pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getSASLHeader()));
+
+        assertThrows(IllegalStateException.class, () -> engine.saslDriver().setMaxFrameSize(1024));
+    }
+
+    @Test
+    public void testCannotSaslDriverChangeMaxFrameSizeSmallerThanSpecMin() {
+        final Engine engine = createSaslServerEngine();
+
+        engine.start();
+
+        assertThrows(IllegalArgumentException.class, () -> engine.saslDriver().setMaxFrameSize(256));
+    }
+
+    @Test
+    public void testCanChangeSaslDriverMaxFrameSizeSmallerThanSpecMin() {
+        final Engine engine = createSaslServerEngine();
+
+        engine.start();
+        engine.saslDriver().setMaxFrameSize(2048);
+
+        assertEquals(2048, engine.saslDriver().getMaxFrameSize());
+    }
+
+    @Test
+    public void testCannotRegisterSaslDriverAfterEngineStarted() {
+        final ProtonEngine engine = (ProtonEngine) EngineFactory.PROTON.createEngine();
+
+        engine.start();
+
+        assertTrue(engine.isRunning());
+        assertThrows(EngineStartedException.class, () -> engine.registerSaslDriver(new EngineSaslDriver() {
+
+            @Override
+            public SaslClientContext client() {
+                return null;
+            }
+
+            @Override
+            public SaslServerContext server() {
+                return null;
+            }
+
+            @Override
+            public SaslState getSaslState() {
+                return null;
+            }
+
+            @Override
+            public SaslOutcome getSaslOutcome() {
+                return null;
+            }
+
+            @Override
+            public int getMaxFrameSize() {
+                return 0;
+            }
+
+            @Override
+            public void setMaxFrameSize(int maxFrameSize) {
+            }
+        }));
+    }
+
+    /**
+     * Test that when the SASL server handler reads an AMQP Header before negotiations
+     * have started it rejects the exchange by sending a SASL Header back to the remote
+     */
+    @Test
+    public void testSaslRejectsAMQPHeader() {
+        final AtomicBoolean headerRead = new AtomicBoolean();
+
+        Engine engine = createSaslServerEngine();
+
+        engine.saslDriver().server().setListener(new SaslServerListener() {
+
+            @Override
+            public void handleSaslResponse(SaslServerContext context, ProtonBuffer response) {
+            }
+
+            @Override
+            public void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse) {
+            }
+
+            @Override
+            public void handleSaslHeader(SaslServerContext context, AMQPHeader header) {
+                headerRead.set(true);
+            }
+        });
+
+        engine.start();
+
+        try {
+            engine.pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getAMQPHeader()));
+            fail("SASL handler should reject a non-SASL AMQP Header read.");
+        } catch (ProtocolViolationException pve) {
+            // Expected
+        }
+
+        assertFalse(headerRead.get(), "Should not receive a Header");
+
+        List<PerformativeEnvelope<?>> frames = testHandler.getFramesWritten();
+
+        assertEquals(1, frames.size(), "Sasl Anonymous exchange output not as expected");
+
+        for (int i = 0; i < frames.size(); ++i) {
+            PerformativeEnvelope<?> frame = frames.get(i);
+            switch (i) {
+                case 0:
+                    assertTrue(frame.getFrameType() == HeaderEnvelope.HEADER_FRAME_TYPE);
+                    HeaderEnvelope header = (HeaderEnvelope) frame;
+                    assertTrue(header.getBody().isSaslHeader(), "Should have written a SASL Header in response");
+                    break;
+                default:
+                    fail("Invalid Frame read during exchange: " + frame);
+            }
+        }
+
+        assertEquals(EngineState.FAILED, engine.state());
+    }
+
+    @Test
+    public void testExchangeSaslHeader() {
+        final AtomicBoolean saslHeaderRead = new AtomicBoolean();
+
+        Engine engine = createSaslServerEngine().start().getEngine();
+
+        engine.saslDriver().server().setListener(new SaslServerListener() {
+
+            @Override
+            public void handleSaslHeader(SaslServerContext context, AMQPHeader header) {
+                if (header.isSaslHeader()) {
+                    saslHeaderRead.set(true);
+                }
+            }
+
+            @Override
+            public void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse) {
+            }
+
+            @Override
+            public void handleSaslResponse(SaslServerContext context, ProtonBuffer response) {
+            }
+        });
+
+        engine.pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getSASLHeader()));
+
+        assertThrows(IllegalStateException.class, () -> engine.saslDriver().client());
+
+        assertTrue(saslHeaderRead.get(), "Did not receive a SASL Header");
+
+        List<PerformativeEnvelope<?>> frames = testHandler.getFramesWritten();
+
+        // We should get a SASL header indicating that the server accepted SASL
+        assertEquals(1, frames.size(), "Sasl Anonymous exchange output not as expected");
+
+        for (int i = 0; i < frames.size(); ++i) {
+            PerformativeEnvelope<?> frame = frames.get(i);
+            switch (i) {
+                case 0:
+                    assertTrue(frame.getFrameType() == HeaderEnvelope.HEADER_FRAME_TYPE);
+                    HeaderEnvelope header = (HeaderEnvelope) frame;
+                    assertTrue(header.getBody().isSaslHeader());
+                    break;
+                case 1:
+                    assertTrue(frame.getFrameType() == SASLEnvelope.SASL_FRAME_TYPE);
+                    break;
+                default:
+                    fail("Invalid Frame read during exchange: " + frame);
+            }
+        }
+    }
+
+    @Test
+    public void testSaslAnonymousExchange() {
+        final AtomicBoolean saslHeaderRead = new AtomicBoolean();
+
+        final AtomicReference<String> clientHostname = new AtomicReference<>();
+        final AtomicReference<Symbol> clientMechanism = new AtomicReference<>();
+        final AtomicBoolean emptyResponse = new AtomicBoolean();
+
+        Engine engine = createSaslServerEngine();
+
+        engine.saslDriver().server().setListener(new SaslServerListener() {
+
+            @Override
+            public void handleSaslHeader(SaslServerContext context, AMQPHeader header) {
+                if (header.isSaslHeader()) {
+                    saslHeaderRead.set(true);
+                }
+
+                context.sendMechanisms(new Symbol[] { Symbol.valueOf("ANONYMOUS") });
+            }
+
+            @Override
+            public void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse) {
+                clientHostname.set(context.getHostname());
+                clientMechanism.set(mechanism);
+                if (initResponse.getReadableBytes() == 0) {
+                    emptyResponse.set(true);
+                }
+
+                context.sendOutcome(SaslOutcome.SASL_OK, null);
+            }
+
+            @Override
+            public void handleSaslResponse(SaslServerContext context, ProtonBuffer response) {
+
+            }
+        });
+
+        // Check for Header processing
+        engine.start().getEngine().pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getSASLHeader()));
+
+        assertTrue(saslHeaderRead.get(), "Did not receive a SASL Header");
+
+        SaslInit clientInit = new SaslInit();
+        clientInit.setHostname("HOST-NAME");
+        clientInit.setMechanism(Symbol.valueOf("ANONYMOUS"));
+        clientInit.setInitialResponse(new Binary(new byte[0]));
+
+        // Check for Initial Response processing
+        engine.pipeline().fireRead(new SASLEnvelope(clientInit));
+
+        assertEquals("HOST-NAME", clientHostname.get());
+        assertEquals(Symbol.valueOf("ANONYMOUS"), clientMechanism.get());
+        assertTrue(emptyResponse.get(), "Response should be an empty byte array");
+
+        List<PerformativeEnvelope<?>> frames = testHandler.getFramesWritten();
+
+        assertEquals(3, frames.size(), "SASL Anonymous exchange output not as expected");
+
+        for (int i = 0; i < frames.size(); ++i) {
+            PerformativeEnvelope<?> frame = frames.get(i);
+            SASLEnvelope saslFrame = null;
+
+            switch (i) {
+                case 0:
+                    assertTrue(frame.getFrameType() == HeaderEnvelope.HEADER_FRAME_TYPE);
+                    HeaderEnvelope header = (HeaderEnvelope) frame;
+                    assertTrue(header.getBody().isSaslHeader());
+                    break;
+                case 1:
+                    assertTrue(frame.getFrameType() == SASLEnvelope.SASL_FRAME_TYPE);
+                    saslFrame = (SASLEnvelope) frame;
+                    assertEquals(SaslPerformative.SaslPerformativeType.MECHANISMS, saslFrame.getBody().getPerformativeType());
+                    SaslMechanisms mechanisms = (SaslMechanisms) saslFrame.getBody();
+                    assertEquals(1, mechanisms.getSaslServerMechanisms().length);
+                    assertEquals(Symbol.valueOf("ANONYMOUS"), mechanisms.getSaslServerMechanisms()[0]);
+                    break;
+                case 2:
+                    assertTrue(frame.getFrameType() == SASLEnvelope.SASL_FRAME_TYPE);
+                    saslFrame = (SASLEnvelope) frame;
+                    assertEquals(SaslPerformative.SaslPerformativeType.OUTCOME, saslFrame.getBody().getPerformativeType());
+                    org.apache.qpid.protonj2.types.security.SaslOutcome outcome =
+                        (org.apache.qpid.protonj2.types.security.SaslOutcome) saslFrame.getBody();
+                    assertEquals(SaslCode.OK, outcome.getCode());
+                    break;
+                default:
+                    fail("Invalid Frame read during exchange: " + frame);
+            }
+        }
+    }
+
+    @Test
+    public void testEngineFailedIfMoreSaslFramesArriveAfterSaslDone() {
+        final AtomicBoolean saslHeaderRead = new AtomicBoolean();
+
+        final AtomicReference<String> clientHostname = new AtomicReference<>();
+        final AtomicReference<Symbol> clientMechanism = new AtomicReference<>();
+        final AtomicBoolean emptyResponse = new AtomicBoolean();
+
+        Engine engine = createSaslServerEngine();
+
+        engine.saslDriver().server().setListener(new SaslServerListener() {
+
+            @Override
+            public void handleSaslHeader(SaslServerContext context, AMQPHeader header) {
+                if (header.isSaslHeader()) {
+                    saslHeaderRead.set(true);
+                }
+
+                context.sendMechanisms(new Symbol[] { Symbol.valueOf("ANONYMOUS") });
+            }
+
+            @Override
+            public void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse) {
+                clientHostname.set(context.getHostname());
+                clientMechanism.set(mechanism);
+                if (initResponse.getReadableBytes() == 0) {
+                    emptyResponse.set(true);
+                }
+
+                context.sendOutcome(SaslOutcome.SASL_OK, null);
+            }
+
+            @Override
+            public void handleSaslResponse(SaslServerContext context, ProtonBuffer response) {
+
+            }
+        });
+
+        // Check for Header processing
+        engine.start().getEngine().pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getSASLHeader()));
+
+        assertTrue(saslHeaderRead.get(), "Did not receive a SASL Header");
+
+        SaslInit clientInit = new SaslInit();
+        clientInit.setHostname("HOST-NAME");
+        clientInit.setMechanism(Symbol.valueOf("ANONYMOUS"));
+        clientInit.setInitialResponse(new Binary(new byte[0]));
+
+        // Check for Initial Response processing
+        engine.pipeline().fireRead(new SASLEnvelope(clientInit));
+
+        assertEquals("HOST-NAME", clientHostname.get());
+        assertEquals(Symbol.valueOf("ANONYMOUS"), clientMechanism.get());
+        assertTrue(emptyResponse.get(), "Response should be an empty byte array");
+
+        List<PerformativeEnvelope<?>> frames = testHandler.getFramesWritten();
+        assertEquals(3, frames.size(), "SASL Anonymous exchange output not as expected");
+
+        assertEquals(engine.saslDriver().getSaslState(), SaslState.AUTHENTICATED);
+
+        // Fire another SASL frame and the engine should fail
+        try {
+            engine.pipeline().fireRead(new SASLEnvelope(clientInit));
+            fail("Server should fail on unexpected SASL frames");
+        } catch (EngineFailedException efe) {
+        }
+
+        assertTrue(engine.isFailed());
+    }
+
+    @Test
+    public void testSaslHandlerDefaultsIntoServerMode() {
+        Engine engine = createSaslCapableEngine();
+
+        // Swallow incoming so we can test that an AMQP Header arrives after SASL
+        engine.pipeline().addFirst("read-sink", new FrameReadSinkTransportHandler());
+
+        // Check for Header processing
+        engine.start().getEngine().pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getSASLHeader()));
+
+        SaslInit clientInit = new SaslInit();
+        clientInit.setHostname("HOST-NAME");
+        clientInit.setMechanism(Symbol.valueOf("ANONYMOUS"));
+        clientInit.setInitialResponse(new Binary(new byte[0]));
+
+        // Check for Initial Response processing
+        engine.pipeline().fireRead(new SASLEnvelope(clientInit));
+
+        List<PerformativeEnvelope<?>> frames = testHandler.getFramesWritten();
+
+        assertEquals(3, frames.size(), "SASL Anonymous exchange output not as expected");
+
+        engine.start().getEngine().pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getAMQPHeader()));
+
+        for (int i = 0; i < frames.size(); ++i) {
+            PerformativeEnvelope<?> frame = frames.get(i);
+            SASLEnvelope saslFrame = null;
+
+            switch (i) {
+                case 0:
+                    assertTrue(frame.getFrameType() == HeaderEnvelope.HEADER_FRAME_TYPE);
+                    HeaderEnvelope header1 = (HeaderEnvelope) frame;
+                    assertTrue(header1.getBody().isSaslHeader());
+                    break;
+                case 1:
+                    assertTrue(frame.getFrameType() == SASLEnvelope.SASL_FRAME_TYPE);
+                    saslFrame = (SASLEnvelope) frame;
+                    assertEquals(SaslPerformative.SaslPerformativeType.MECHANISMS, saslFrame.getBody().getPerformativeType());
+                    SaslMechanisms mechanisms = (SaslMechanisms) saslFrame.getBody();
+                    assertEquals(1, mechanisms.getSaslServerMechanisms().length);
+                    assertEquals(Symbol.valueOf("PLAIN"), mechanisms.getSaslServerMechanisms()[0]);
+                    break;
+                case 2:
+                    assertTrue(frame.getFrameType() == SASLEnvelope.SASL_FRAME_TYPE);
+                    saslFrame = (SASLEnvelope) frame;
+                    assertEquals(SaslPerformative.SaslPerformativeType.OUTCOME, saslFrame.getBody().getPerformativeType());
+                    org.apache.qpid.protonj2.types.security.SaslOutcome outcome =
+                        (org.apache.qpid.protonj2.types.security.SaslOutcome) saslFrame.getBody();
+                    assertEquals(SaslCode.AUTH, outcome.getCode());
+                    break;
+                case 3:
+                    assertTrue(frame.getFrameType() == HeaderEnvelope.HEADER_FRAME_TYPE);
+                    HeaderEnvelope header2 = (HeaderEnvelope) frame;
+                    assertFalse(header2.getBody().isSaslHeader());
+                    break;
+                default:
+                    fail("Invalid Frame read during exchange: " + frame);
+            }
+        }
+    }
+
+    @Test
+    public void testEngineFailedWhenNonSaslFrameWrittenDuringSaslExchange() {
+        final AtomicBoolean saslHeaderRead = new AtomicBoolean();
+
+        final AtomicReference<String> clientHostname = new AtomicReference<>();
+        final AtomicReference<Symbol> clientMechanism = new AtomicReference<>();
+
+        Engine engine = createSaslServerEngine();
+
+        engine.saslDriver().server().setListener(new SaslServerListener() {
+
+            @Override
+            public void handleSaslHeader(SaslServerContext context, AMQPHeader header) {
+                if (header.isSaslHeader()) {
+                    saslHeaderRead.set(true);
+                }
+
+                context.sendMechanisms(new Symbol[] { Symbol.valueOf("ANONYMOUS") });
+            }
+
+            @Override
+            public void handleSaslInit(SaslServerContext context, Symbol mechanism, ProtonBuffer initResponse) {
+                clientHostname.set(context.getHostname());
+                clientMechanism.set(mechanism);
+            }
+
+            @Override
+            public void handleSaslResponse(SaslServerContext context, ProtonBuffer response) {
+
+            }
+        });
+
+        // Check for Header processing
+        engine.start().getEngine().pipeline().fireRead(new HeaderEnvelope(AMQPHeader.getSASLHeader()));
+
+        assertTrue(saslHeaderRead.get(), "Did not receive a SASL Header");
+
+        SaslInit clientInit = new SaslInit();
+        clientInit.setHostname("HOST-NAME");
+        clientInit.setMechanism(Symbol.valueOf("ANONYMOUS"));
+        clientInit.setInitialResponse(new Binary(new byte[0]));
+
+        // Check for Initial Response processing
+        engine.pipeline().fireRead(new SASLEnvelope(clientInit));
+
+        assertEquals("HOST-NAME", clientHostname.get());
+        assertEquals(Symbol.valueOf("ANONYMOUS"), clientMechanism.get());
+
+        List<PerformativeEnvelope<?>> frames = testHandler.getFramesWritten();
+
+        assertEquals(2, frames.size(), "SASL Anonymous exchange output not as expected");
+
+        try {
+            engine.pipeline().fireWrite(AMQPPerformativeEnvelopePool.outgoingEnvelopePool().take(new Open(), 0, null));
+        } catch (ProtocolViolationException pvex) {}
+
+        assertTrue(engine.isFailed());
+
+        for (int i = 0; i < frames.size(); ++i) {
+            PerformativeEnvelope<?> frame = frames.get(i);
+            SASLEnvelope saslFrame = null;
+
+            switch (i) {
+                case 0:
+                    assertTrue(frame.getFrameType() == HeaderEnvelope.HEADER_FRAME_TYPE);
+                    HeaderEnvelope header = (HeaderEnvelope) frame;
+                    assertTrue(header.getBody().isSaslHeader());
+                    break;
+                case 1:
+                    assertTrue(frame.getFrameType() == SASLEnvelope.SASL_FRAME_TYPE);
+                    saslFrame = (SASLEnvelope) frame;
+                    assertEquals(SaslPerformative.SaslPerformativeType.MECHANISMS, saslFrame.getBody().getPerformativeType());
+                    SaslMechanisms mechanisms = (SaslMechanisms) saslFrame.getBody();
+                    assertEquals(1, mechanisms.getSaslServerMechanisms().length);
+                    assertEquals(Symbol.valueOf("ANONYMOUS"), mechanisms.getSaslServerMechanisms()[0]);
+                    break;
+                default:
+                    fail("Invalid Frame read during exchange: " + frame);
+            }
+        }
+    }
+
+    private Engine createSaslServerEngine() {
+        ProtonEngine engine = new ProtonEngine();
+
+        engine.pipeline().addLast("sasl", new ProtonSaslHandler());
+        engine.pipeline().addLast("test", testHandler);
+        engine.pipeline().addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        // Ensure engine SASL driver is configured for server mode.
+        engine.saslDriver().server();
+
+        return engine;
+    }
+
+    private Engine createSaslClientEngine() {
+        ProtonEngine engine = new ProtonEngine();
+
+        engine.pipeline().addLast("sasl", new ProtonSaslHandler());
+        engine.pipeline().addLast("test", testHandler);
+        engine.pipeline().addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        // Ensure engine SASL driver is configured for client mode.
+        engine.saslDriver().client();
+
+        return engine;
+    }
+
+    private Engine createSaslCapableEngine() {
+        ProtonEngine engine = new ProtonEngine();
+
+        engine.pipeline().addLast("sasl", new ProtonSaslHandler());
+        engine.pipeline().addLast("test", testHandler);
+        engine.pipeline().addLast("write-sink", new FrameWriteSinkTransportHandler());
+
+        return engine;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractMechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractMechanismTest.java
new file mode 100644
index 0000000..f31bc4e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractMechanismTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class AbstractMechanismTest {
+
+    @Test
+    public void testToStringCarriesMechName() {
+        TestMechanism mech = new TestMechanism();
+
+        assertTrue(mech.toString().contains("TEST"));
+    }
+
+    private static class TestMechanism extends AbstractMechanism {
+
+        @Override
+        public Symbol getName() {
+            return Symbol.valueOf("TEST");
+        }
+
+        @Override
+        public boolean isApplicable(SaslCredentialsProvider credentials) {
+            return true;
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractScramSHAMechanismTestBase.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractScramSHAMechanismTestBase.java
new file mode 100644
index 0000000..1915efb
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AbstractScramSHAMechanismTestBase.java
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The quoted text in the test method javadoc is taken from RFC 5802.
+ */
+public abstract class AbstractScramSHAMechanismTestBase extends MechanismTestBase {
+
+    private final ProtonBuffer expectedClientInitialResponse;
+    private final ProtonBuffer serverFirstMessage;
+    private final ProtonBuffer expectedClientFinalMessage;
+    private final ProtonBuffer serverFinalMessage;
+
+    public AbstractScramSHAMechanismTestBase(ProtonBuffer expectedClientInitialResponse,
+                                             ProtonBuffer serverFirstMessage,
+                                             ProtonBuffer expectedClientFinalMessage,
+                                             ProtonBuffer serverFinalMessage) {
+
+        this.expectedClientInitialResponse = expectedClientInitialResponse;
+        this.serverFirstMessage = serverFirstMessage;
+        this.expectedClientFinalMessage = expectedClientFinalMessage;
+        this.serverFinalMessage = serverFinalMessage;
+    }
+
+    protected abstract Mechanism getMechanismForTesting();
+
+    protected abstract SaslCredentialsProvider getTestCredentials();
+
+    @Test
+    public void testSuccessfulAuthentication() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+
+        ProtonBuffer clientInitialResponse = mechanism.getInitialResponse(getTestCredentials());
+        assertEquals(expectedClientInitialResponse, clientInitialResponse);
+
+        ProtonBuffer clientFinalMessage = mechanism.getChallengeResponse(getTestCredentials(), serverFirstMessage);
+        assertEquals(expectedClientFinalMessage, clientFinalMessage);
+
+        ProtonBuffer expectedFinalChallengeResponse = ProtonByteBufferAllocator.DEFAULT.wrap("".getBytes());
+        assertEquals(expectedFinalChallengeResponse, mechanism.getChallengeResponse(getTestCredentials(), serverFinalMessage));
+
+        mechanism.verifyCompletion();
+    }
+
+    @Test
+    public void testServerFirstMessageMalformed() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+
+        mechanism.getInitialResponse(getTestCredentials());
+
+        ProtonBuffer challenge = ProtonByteBufferAllocator.DEFAULT.wrap("badserverfirst".getBytes());
+        challenge.setIndex(0, challenge.capacity());
+
+        try {
+            mechanism.getChallengeResponse(getTestCredentials(), challenge);
+            fail("Exception not thrown");
+        } catch (SaslException s) {
+            // PASS
+        }
+    }
+
+    /**
+     * 5.1.  SCRAM Attributes
+     * "m: This attribute is reserved for future extensibility.  In this
+     * version of SCRAM, its presence in a client or a server message
+     * MUST cause authentication failure when the attribute is parsed by
+     * the other end."
+     *
+     * @throws Exception if an unexpected exception is thrown.
+     */
+    @Test
+    public void testServerFirstMessageMandatoryExtensionRejected() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+
+        mechanism.getInitialResponse(getTestCredentials());
+
+        ProtonBuffer challenge = ProtonByteBufferAllocator.DEFAULT.wrap("m=notsupported,s=,i=".getBytes());
+        challenge.setIndex(0, challenge.capacity());
+
+        try {
+            mechanism.getChallengeResponse(getTestCredentials(), challenge);
+            fail("Exception not thrown");
+        } catch (SaslException s) {
+            // PASS
+        }
+    }
+
+    /**
+     * 5.  SCRAM Authentication Exchange
+     * "In [the server first] response, the server sends a "server-first-message" containing the
+     * user's iteration count i and the user's salt, and appends its own
+     * nonce to the client-specified one."
+     *
+     * @throws Exception if an unexpected exception is thrown.
+     */
+    @Test
+    public void testServerFirstMessageInvalidNonceRejected() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+
+        mechanism.getInitialResponse(getTestCredentials());
+
+        ProtonBuffer challenge = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "r=invalidnonce,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096".getBytes());
+        challenge.setIndex(0, challenge.capacity());
+
+        try {
+            mechanism.getChallengeResponse(getTestCredentials(), challenge);
+            fail("Exception not thrown");
+        } catch (SaslException s) {
+            // PASS
+        }
+    }
+
+    /**
+     * 5.  SCRAM Authentication Exchange
+     * "The client then authenticates the server by computing the
+     * ServerSignature and comparing it to the value sent by the server.  If
+     * the two are different, the client MUST consider the authentication
+     * exchange to be unsuccessful, and it might have to drop the
+     * connection."
+     *
+     * @throws Exception if an unexpected exception is thrown.
+     */
+    @Test
+    public void testServerSignatureDiffer() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+
+        mechanism.getInitialResponse(getTestCredentials());
+        mechanism.getChallengeResponse(getTestCredentials(), serverFirstMessage);
+
+        ProtonBuffer challenge = ProtonByteBufferAllocator.DEFAULT.wrap("v=badserverfinal".getBytes());
+        challenge.setIndex(0, challenge.capacity());
+
+        try {
+            mechanism.getChallengeResponse(getTestCredentials(), challenge);
+            fail("Exception not thrown");
+        } catch (SaslException e) {
+            // PASS
+        }
+    }
+
+    @Test
+    public void testIncompleteExchange() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+
+        ProtonBuffer clientInitialResponse = mechanism.getInitialResponse(getTestCredentials());
+        assertEquals(expectedClientInitialResponse, clientInitialResponse);
+
+        ProtonBuffer clientFinalMessage = mechanism.getChallengeResponse(getTestCredentials(), serverFirstMessage);
+        assertEquals(expectedClientFinalMessage, clientFinalMessage);
+
+        try {
+            mechanism.verifyCompletion();
+            fail("Exception not thrown");
+        } catch (SaslException e) {
+            // PASS
+        }
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AnonymousMechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AnonymousMechanismTest.java
new file mode 100644
index 0000000..f875651
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/AnonymousMechanismTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.junit.jupiter.api.Test;
+
+public class AnonymousMechanismTest extends MechanismTestBase {
+
+    @Test
+    public void testGetInitialResponseWithNullUserAndPassword() throws SaslException {
+        AnonymousMechanism mech = new AnonymousMechanism();
+
+        ProtonBuffer response = mech.getInitialResponse(credentials());
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testGetChallengeResponse() throws SaslException {
+        AnonymousMechanism mech = new AnonymousMechanism();
+
+        ProtonBuffer response = mech.getChallengeResponse(credentials(), TEST_BUFFER);
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testIsApplicableWithNoCredentials() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials(null, null, false)),
+            "Should be applicable with no credentials");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoUser() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials(null, "pass", false)),
+            "Should be applicable with no username");
+    }
+
+    @Test
+    public void testIsApplicableWithNoPassword() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials("user", null, false)),
+            "Should be applicable with no password");
+    }
+
+    @Test
+    public void testIsApplicableWithEmtpyUser() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials("", "pass", false)),
+            "Should be applicable with empty username");
+    }
+
+    @Test
+    public void testIsApplicableWithEmtpyPassword() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials("user", "", false)),
+            "Should be applicable with empty password");
+    }
+
+    @Test
+    public void testIsApplicableWithEmtpyUserAndPassword() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials("", "", false)),
+            "Should be applicable with empty user and password");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPassword() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials("user", "password", false)),
+            "Should be applicable with user and password");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPasswordAndPrincipal() {
+        assertTrue(SaslMechanisms.ANONYMOUS.createMechanism().isApplicable(credentials("user", "password", true)),
+            "Should be applicable with user and password and principal");
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/CramMD5MechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/CramMD5MechanismTest.java
new file mode 100644
index 0000000..1829aa8
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/CramMD5MechanismTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Base64;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The known good used by these tests is taken from the example in RFC 2195 section 2.
+ */
+public class CramMD5MechanismTest extends MechanismTestBase {
+
+    private final ProtonBuffer SERVER_FIRST_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        Base64.getDecoder().decode("PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+"));
+
+    private final ProtonBuffer EXPECTED_CLIENT_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        Base64.getDecoder().decode("dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw"));
+
+    private static final String TEST_USERNAME = "tim";
+    private static final String TEST_PASSWORD = "tanstaaftanstaaf";
+
+    @Test
+    public void testSuccessfulAuthentication() throws Exception {
+        Mechanism mechanism = new CramMD5Mechanism();
+        SaslCredentialsProvider creds = credentials(TEST_USERNAME, TEST_PASSWORD);
+
+        ProtonBuffer clientInitialResponse = mechanism.getInitialResponse(creds);
+        assertNull(clientInitialResponse);
+
+        ProtonBuffer clientFinalResponse = mechanism.getChallengeResponse(creds, SERVER_FIRST_MESSAGE);
+        assertEquals(EXPECTED_CLIENT_FINAL_MESSAGE, clientFinalResponse);
+
+        mechanism.verifyCompletion();
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoCredentials() {
+        assertFalse(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials(null, null, false)),
+            "Should not be applicable with no credentials");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoUser() {
+        assertFalse(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials(null, "pass", false)),
+            "Should not be applicable with no username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoPassword() {
+        assertFalse(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials("user", null, false)),
+            "Should not be applicable with no password");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUser() {
+        assertFalse(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials("", "pass", false)),
+            "Should not be applicable with empty username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyPassword() {
+        assertFalse(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials("user", "", false)),
+            "Should not be applicable with empty password");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUserAndPassword() {
+        assertFalse(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials("", "", false)),
+            "Should not be applicable with empty user and password");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPassword() {
+        assertTrue(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials("user", "pass", false)),
+            "Should be applicable with user and password");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPasswordAndPrincipal() {
+        assertTrue(SaslMechanisms.CRAM_MD5.createMechanism().isApplicable(credentials("user", "pass", true)),
+            "Should be applicable with user and password and principal");
+    }
+
+    @Test
+    public void testIncompleteExchange() throws Exception {
+        Mechanism mechanism = new CramMD5Mechanism();
+
+        mechanism.getInitialResponse(credentials(TEST_USERNAME, TEST_PASSWORD));
+
+        try {
+            mechanism.verifyCompletion();
+            fail("Exception not thrown");
+        } catch (SaslException e) {
+            // PASS
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ExternalMechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ExternalMechanismTest.java
new file mode 100644
index 0000000..7e7450f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ExternalMechanismTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.junit.jupiter.api.Test;
+
+public class ExternalMechanismTest extends MechanismTestBase {
+
+    @Test
+    public void testGetInitialResponseWithNullUserAndPassword() throws SaslException {
+        ExternalMechanism mech = new ExternalMechanism();
+
+        ProtonBuffer response = mech.getInitialResponse(credentials());
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testGetChallengeResponse() throws SaslException {
+        ExternalMechanism mech = new ExternalMechanism();
+
+        ProtonBuffer response = mech.getChallengeResponse(credentials(), TEST_BUFFER);
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testIsNotApplicableWithUserAndPasswordButNoPrincipal() {
+        assertFalse(SaslMechanisms.EXTERNAL.createMechanism().isApplicable(credentials("user", "password", false)),
+            "Should not be applicable with user and password but no principal");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPasswordAndPrincipal() {
+        assertTrue(SaslMechanisms.EXTERNAL.createMechanism().isApplicable(credentials("user", "password", true)),
+            "Should be applicable with user and password and principal");
+    }
+
+    @Test
+    public void testIsApplicableWithPrincipalOnly() {
+        assertTrue(SaslMechanisms.EXTERNAL.createMechanism().isApplicable(credentials(null, null, true)),
+            "Should be applicable with principal only");
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java
new file mode 100644
index 0000000..121186d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import java.security.Principal;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+
+/**
+ * Base class for SASL Mechanism tests that provides some default utilities
+ */
+public class MechanismTestBase {
+
+    protected static final String HOST = "localhost";
+    protected static final String USERNAME = "user";
+    protected static final String PASSWORD = "pencil";
+
+    protected static final ProtonBuffer TEST_BUFFER = ProtonByteBufferAllocator.DEFAULT.allocate(10, 10).setWriteIndex(10);
+
+    protected SaslCredentialsProvider credentials() {
+        return new UserCredentialsProvider(USERNAME, PASSWORD, HOST, true);
+    }
+
+    protected SaslCredentialsProvider credentials(String user, String password) {
+        return new UserCredentialsProvider(user, password, null, false);
+    }
+
+    protected SaslCredentialsProvider credentials(String user, String password, boolean principal) {
+        return new UserCredentialsProvider(user, password, null, principal);
+    }
+
+    protected SaslCredentialsProvider emptyCredentials() {
+        return new UserCredentialsProvider(null, null, null, false);
+    }
+
+    private static class UserCredentialsProvider implements SaslCredentialsProvider {
+
+        private final String username;
+        private final String password;
+        private final String host;
+        private final boolean principal;
+
+        public UserCredentialsProvider(String username, String password, String host, boolean principal) {
+            this.username = username;
+            this.password = password;
+            this.host = host;
+            this.principal = principal;
+        }
+
+        @Override
+        public String vhost() {
+            return host;
+        }
+
+        @Override
+        public String username() {
+            return username;
+        }
+
+        @Override
+        public String password() {
+            return password;
+        }
+
+        @Override
+        public Principal localPrincipal() {
+            if (principal) {
+                return new Principal() {
+
+                    @Override
+                    public String getName() {
+                        return "TEST-Principal";
+                    }
+                };
+            } else {
+                return null;
+            }
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/PlainMechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/PlainMechanismTest.java
new file mode 100644
index 0000000..96a90a6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/PlainMechanismTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.junit.jupiter.api.Test;
+
+public class PlainMechanismTest extends MechanismTestBase {
+
+    @Test
+    public void testGetInitialResponseWithNullUserAndPassword() throws SaslException {
+        PlainMechanism mech = new PlainMechanism();
+
+        ProtonBuffer response = mech.getInitialResponse(credentials());
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() != 0);
+    }
+
+    @Test
+    public void testGetChallengeResponse() throws SaslException {
+        PlainMechanism mech = new PlainMechanism();
+
+        ProtonBuffer response = mech.getChallengeResponse(credentials(), TEST_BUFFER);
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoCredentials() {
+        assertFalse(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials(null, null, false)),
+            "Should not be applicable with no credentials");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoUser() {
+        assertFalse(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials(null, "pass", false)),
+            "Should not be applicable with no username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoPassword() {
+        assertFalse(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials("user", null, false)),
+            "Should not be applicable with no password");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUser() {
+        assertFalse(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials("", "pass", false)),
+            "Should not be applicable with empty username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyPassword() {
+        assertFalse(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials("user", "", false)),
+            "Should not be applicable with empty password");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUserAndPassword() {
+        assertFalse(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials("", "", false)),
+            "Should not be applicable with empty user and password");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPassword() {
+        assertTrue(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials("user", "pass", false)),
+            "Should be applicable with user and password");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPasswordAndPrincipal() {
+        assertTrue(SaslMechanisms.PLAIN.createMechanism().isApplicable(credentials("user", "pass", true)),
+            "Should be applicable with user and password and principal");
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismSelectorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismSelectorTest.java
new file mode 100644
index 0000000..2352425
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismSelectorTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class SaslMechanismSelectorTest extends MechanismTestBase {
+
+    private static final Symbol[] TEST_MECHANISMS_ARRAY = { ExternalMechanism.EXTERNAL,
+                                                            CramMD5Mechanism.CRAM_MD5,
+                                                            PlainMechanism.PLAIN,
+                                                            AnonymousMechanism.ANONYMOUS };
+
+    @Test
+    public void testSelectAnonymousFromAll() {
+        SaslMechanismSelector selector = new SaslMechanismSelector();
+
+        Mechanism mech = selector.select(TEST_MECHANISMS_ARRAY, emptyCredentials());
+
+        assertNotNull(mech);
+        assertEquals(AnonymousMechanism.ANONYMOUS, mech.getName());
+    }
+
+    @Test
+    public void testSelectPlain() {
+        SaslMechanismSelector selector = new SaslMechanismSelector();
+
+        Mechanism mech = selector.select(new Symbol[] { PlainMechanism.PLAIN, AnonymousMechanism.ANONYMOUS }, credentials());
+
+        assertNotNull(mech);
+        assertEquals(PlainMechanism.PLAIN, mech.getName());
+    }
+
+    @Test
+    public void testSelectCramMD5() {
+        SaslMechanismSelector selector = new SaslMechanismSelector();
+
+        Mechanism mech = selector.select(TEST_MECHANISMS_ARRAY, credentials(USERNAME, PASSWORD));
+
+        assertNotNull(mech);
+        assertEquals(CramMD5Mechanism.CRAM_MD5, mech.getName());
+    }
+
+    @Test
+    public void testSelectExternalIfPrincipalAvailable() {
+        SaslMechanismSelector selector = new SaslMechanismSelector();
+
+        Mechanism mech = selector.select(TEST_MECHANISMS_ARRAY, credentials());
+
+        assertNotNull(mech);
+        assertEquals(ExternalMechanism.EXTERNAL, mech.getName());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismsTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismsTest.java
new file mode 100644
index 0000000..e92ebce
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanismsTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class SaslMechanismsTest {
+
+    @Test
+    public void testValueOfAnonymous() {
+        SaslMechanisms mech = SaslMechanisms.valueOf(AnonymousMechanism.ANONYMOUS);
+        assertNotNull(mech);
+        assertEquals(SaslMechanisms.ANONYMOUS, mech);
+    }
+
+    @Test
+    public void testRequestInvalidMechanismName() {
+        try {
+            SaslMechanisms.valueOf(Symbol.valueOf("TEST"));
+            fail("Should throw when invalid mechanism name given.");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA1MechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA1MechanismTest.java
new file mode 100644
index 0000000..b9716e6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA1MechanismTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The known good used by these tests is taken from the example in RFC 5802 section 5.
+ */
+public class ScramSHA1MechanismTest extends AbstractScramSHAMechanismTestBase {
+
+    private static final String TEST_USERNAME = "user";
+    private static final String TEST_PASSWORD = "pencil";
+
+    private static final String CLIENT_NONCE = "fyko+d2lbbFgONRv9qkxdawL";
+
+    private static final ProtonBuffer EXPECTED_CLIENT_INITIAL_RESPONSE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer SERVER_FIRST_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer EXPECTED_CLIENT_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer SERVER_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "v=rmF9pqV8S7suAoZWja4dJRkFsKQ=".getBytes(StandardCharsets.UTF_8));
+
+    public ScramSHA1MechanismTest() {
+        super(EXPECTED_CLIENT_INITIAL_RESPONSE,
+              SERVER_FIRST_MESSAGE,
+              EXPECTED_CLIENT_FINAL_MESSAGE,
+              SERVER_FINAL_MESSAGE);
+    }
+
+    @Override
+    protected SaslCredentialsProvider getTestCredentials() {
+        return credentials(TEST_USERNAME, TEST_PASSWORD);
+    }
+
+    @Override
+    protected Mechanism getMechanismForTesting() {
+        return new ScramSHA1Mechanism(CLIENT_NONCE);
+    }
+
+    @Test
+    public void testGetNameMatchesValueInSaslMechanismsEnum() {
+        assertEquals(SaslMechanisms.SCRAM_SHA_1.getName(), getMechanismForTesting().getName());
+    }
+
+    @Test
+    public void testDifferentClientNonceOnEachInstance() throws Exception {
+        ScramSHA1Mechanism mech1 = new ScramSHA1Mechanism();
+        ScramSHA1Mechanism mech2 = new ScramSHA1Mechanism();
+
+        ProtonBuffer clientInitialResponse1 = mech1.getInitialResponse(getTestCredentials());
+        ProtonBuffer clientInitialResponse2 = mech2.getInitialResponse(getTestCredentials());
+
+        assertTrue(clientInitialResponse1.toString(StandardCharsets.UTF_8).startsWith("n,,n=user,r="));
+        assertTrue(clientInitialResponse2.toString(StandardCharsets.UTF_8).startsWith("n,,n=user,r="));
+
+        assertThat(clientInitialResponse1, not(equalTo(clientInitialResponse2)));
+    }
+
+    @Test
+    public void testUsernameCommaEqualsCharactersEscaped() throws Exception {
+        String originalUsername = "user,name=";
+        String escapedUsername = "user=2Cname=3D";
+
+        String expectedInitialResponseString = "n,,n=" + escapedUsername + ",r=" + CLIENT_NONCE;
+        ProtonBuffer expectedInitialResponseBuffer = ProtonByteBufferAllocator.DEFAULT.wrap(
+            expectedInitialResponseString.getBytes(StandardCharsets.UTF_8));
+
+        ScramSHA1Mechanism mech = new ScramSHA1Mechanism(CLIENT_NONCE);
+
+        ProtonBuffer clientInitialResponse = mech.getInitialResponse(credentials(originalUsername, "password"));
+        assertEquals(expectedInitialResponseBuffer, clientInitialResponse);
+    }
+
+    @Test
+    public void testPasswordCommaEqualsCharactersNotEscaped() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+        SaslCredentialsProvider credentials = credentials(TEST_USERNAME, TEST_PASSWORD + ",=");
+
+        ProtonBuffer clientInitialResponse = mechanism.getInitialResponse(credentials);
+        assertEquals(EXPECTED_CLIENT_INITIAL_RESPONSE, clientInitialResponse);
+
+        ProtonBuffer serverFirstMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "r=fyko+d2lbbFgONRv9qkxdawLdcbfa301-1618-46ee-96c1-2bf60139dc7f,s=Q0zM1qzKMOmI0sAzE7dXt6ru4ZIXhAzn40g4mQXKQdw=,i=4096".getBytes(StandardCharsets.UTF_8));
+        ProtonBuffer expectedClientFinalMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "c=biws,r=fyko+d2lbbFgONRv9qkxdawLdcbfa301-1618-46ee-96c1-2bf60139dc7f,p=quRNWvZqGUvPXoazebZe0ZYsjQI=".getBytes(StandardCharsets.UTF_8));
+
+        ProtonBuffer clientFinalMessage = mechanism.getChallengeResponse(credentials, serverFirstMessage);
+
+        assertEquals(expectedClientFinalMessage, clientFinalMessage);
+
+        ProtonBuffer serverFinalMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "v=dnJDHm3fp6WwVrl5yjZuqKp03lQ=".getBytes(StandardCharsets.UTF_8));
+        ProtonBuffer expectedFinalChallengeResponse = ProtonByteBufferAllocator.DEFAULT.wrap("".getBytes());
+
+        assertEquals(expectedFinalChallengeResponse, mechanism.getChallengeResponse(credentials, serverFinalMessage));
+
+        mechanism.verifyCompletion();
+    }
+
+}
\ No newline at end of file
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA256MechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA256MechanismTest.java
new file mode 100644
index 0000000..da99354
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA256MechanismTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The known good used by these tests is taken from the example in RFC 7677 section 3.
+ */
+public class ScramSHA256MechanismTest extends AbstractScramSHAMechanismTestBase {
+
+    private static final String TEST_USERNAME = "user";
+    private static final String TEST_PASSWORD = "pencil";
+
+    private static final String CLIENT_NONCE = "rOprNGfwEbeRWgbNEkqO";
+
+    private static final ProtonBuffer EXPECTED_CLIENT_INITIAL_RESPONSE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "n,,n=user,r=rOprNGfwEbeRWgbNEkqO".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer SERVER_FIRST_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer EXPECTED_CLIENT_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer SERVER_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=".getBytes(StandardCharsets.UTF_8));
+
+    public ScramSHA256MechanismTest() {
+        super(EXPECTED_CLIENT_INITIAL_RESPONSE,
+              SERVER_FIRST_MESSAGE,
+              EXPECTED_CLIENT_FINAL_MESSAGE,
+              SERVER_FINAL_MESSAGE);
+    }
+
+    @Override
+    protected SaslCredentialsProvider getTestCredentials() {
+        return credentials(TEST_USERNAME, TEST_PASSWORD);
+    }
+
+    @Override
+    protected Mechanism getMechanismForTesting() {
+        return new ScramSHA256Mechanism(CLIENT_NONCE);
+    }
+
+    @Test
+    public void testGetNameMatchesValueInSaslMechanismsEnum() {
+        assertEquals(SaslMechanisms.SCRAM_SHA_256.getName(), getMechanismForTesting().getName());
+    }
+
+    @Test
+    public void testDifferentClientNonceOnEachInstance() throws Exception {
+        ScramSHA256Mechanism mech1 = new ScramSHA256Mechanism();
+        ScramSHA256Mechanism mech2 = new ScramSHA256Mechanism();
+
+        ProtonBuffer clientInitialResponse1 = mech1.getInitialResponse(getTestCredentials());
+        ProtonBuffer clientInitialResponse2 = mech2.getInitialResponse(getTestCredentials());
+
+        assertTrue(clientInitialResponse1.toString(StandardCharsets.UTF_8).startsWith("n,,n=user,r="));
+        assertTrue(clientInitialResponse2.toString(StandardCharsets.UTF_8).startsWith("n,,n=user,r="));
+
+        assertThat(clientInitialResponse1, not(equalTo(clientInitialResponse2)));
+    }
+
+    @Test
+    public void testUsernameCommaEqualsCharactersEscaped() throws Exception {
+        String originalUsername = "user,name=";
+        String escapedUsername = "user=2Cname=3D";
+
+        String expectedInitialResponseString = "n,,n=" + escapedUsername + ",r=" + CLIENT_NONCE;
+        ProtonBuffer expectedInitialResponseBuffer = ProtonByteBufferAllocator.DEFAULT.wrap(
+            expectedInitialResponseString.getBytes(StandardCharsets.UTF_8));
+
+        ScramSHA256Mechanism mech = new ScramSHA256Mechanism(CLIENT_NONCE);
+
+        ProtonBuffer clientInitialResponse = mech.getInitialResponse(credentials(originalUsername, "password"));
+        assertEquals(expectedInitialResponseBuffer, clientInitialResponse);
+    }
+
+    @Test
+    public void testPasswordCommaEqualsCharactersNotEscaped() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+        SaslCredentialsProvider credentials = credentials(TEST_USERNAME, TEST_PASSWORD + ",=");
+
+        ProtonBuffer clientInitialResponse = mechanism.getInitialResponse(credentials);
+        assertEquals(EXPECTED_CLIENT_INITIAL_RESPONSE, clientInitialResponse);
+
+        ProtonBuffer serverFirstMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "r=rOprNGfwEbeRWgbNEkqOb291012f-b281-47d3-acbc-fefffaad60f2,s=fQwuXmWB4XES7vNK4oBlLtH9cbWAmtxO+Z+tZ9m5W54=,i=4096".getBytes(StandardCharsets.UTF_8));
+        ProtonBuffer expectedClientFinalMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "c=biws,r=rOprNGfwEbeRWgbNEkqOb291012f-b281-47d3-acbc-fefffaad60f2,p=PNeUNfKwyqBPjMssgF7yk4iLt8W24NS/D99HjBbXwyw=".getBytes(StandardCharsets.UTF_8));
+
+        ProtonBuffer clientFinalMessage = mechanism.getChallengeResponse(credentials, serverFirstMessage);
+
+        assertEquals(expectedClientFinalMessage, clientFinalMessage);
+
+        ProtonBuffer serverFinalMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "v=/N9SY26AOvz2QZkJZkyXpomWknaFWSN6zBGqg5RNG9w=".getBytes(StandardCharsets.UTF_8));
+        ProtonBuffer expectedFinalChallengeResponse = ProtonByteBufferAllocator.DEFAULT.wrap("".getBytes());
+
+        assertEquals(expectedFinalChallengeResponse, mechanism.getChallengeResponse(credentials, serverFinalMessage));
+
+        mechanism.verifyCompletion();
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA512MechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA512MechanismTest.java
new file mode 100644
index 0000000..54cc490
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/ScramSHA512MechanismTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The known good used by these tests is taken from the example in RFC 7677 section 3.
+ */
+public class ScramSHA512MechanismTest extends AbstractScramSHAMechanismTestBase {
+
+    private static final String TEST_USERNAME = "user";
+    private static final String TEST_PASSWORD = "pencil";
+
+    private static final String CLIENT_NONCE = "rOprNGfwEbeRWgbNEkqO";
+
+    private static final ProtonBuffer EXPECTED_CLIENT_INITIAL_RESPONSE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "n,,n=user,r=rOprNGfwEbeRWgbNEkqO".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer SERVER_FIRST_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "r=rOprNGfwEbeRWgbNEkqO02431b08-2f89-4bad-a4e6-80c0564ec865,s=Yin2FuHTt/M0kJWb0t9OI32n2VmOGi3m+JfjOvuDF88=,i=4096".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer EXPECTED_CLIENT_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "c=biws,r=rOprNGfwEbeRWgbNEkqO02431b08-2f89-4bad-a4e6-80c0564ec865,p=Hc5yec3NmCD7t+kFRw4/3yD6/F3SQHc7AVYschRja+Bc3sbdjlA0eH1OjJc0DD4ghn1tnXN5/Wr6qm9xmaHt4A==".getBytes(StandardCharsets.UTF_8));
+    private static final ProtonBuffer SERVER_FINAL_MESSAGE = ProtonByteBufferAllocator.DEFAULT.wrap(
+        "v=BQuhnKHqYDwQWS5jAw4sZed+C9KFUALsbrq81bB0mh+bcUUbbMPNNmBIupnS2AmyyDnG5CTBQtkjJ9kyY4kzmw==".getBytes(StandardCharsets.UTF_8));
+
+    public ScramSHA512MechanismTest() {
+        super(EXPECTED_CLIENT_INITIAL_RESPONSE,
+              SERVER_FIRST_MESSAGE,
+              EXPECTED_CLIENT_FINAL_MESSAGE,
+              SERVER_FINAL_MESSAGE);
+    }
+
+    @Override
+    protected SaslCredentialsProvider getTestCredentials() {
+        return credentials(TEST_USERNAME, TEST_PASSWORD);
+    }
+
+    @Override
+    protected Mechanism getMechanismForTesting() {
+        return new ScramSHA512Mechanism(CLIENT_NONCE);
+    }
+
+    @Test
+    public void testGetNameMatchesValueInSaslMechanismsEnum() {
+        assertEquals(SaslMechanisms.SCRAM_SHA_512.getName(), getMechanismForTesting().getName());
+    }
+
+    @Test
+    public void testDifferentClientNonceOnEachInstance() throws Exception {
+        ScramSHA512Mechanism mech1 = new ScramSHA512Mechanism();
+        ScramSHA512Mechanism mech2 = new ScramSHA512Mechanism();
+
+        ProtonBuffer clientInitialResponse1 = mech1.getInitialResponse(getTestCredentials());
+        ProtonBuffer clientInitialResponse2 = mech2.getInitialResponse(getTestCredentials());
+
+        assertTrue(clientInitialResponse1.toString(StandardCharsets.UTF_8).startsWith("n,,n=user,r="));
+        assertTrue(clientInitialResponse2.toString(StandardCharsets.UTF_8).startsWith("n,,n=user,r="));
+
+        assertThat(clientInitialResponse1, not(equalTo(clientInitialResponse2)));
+    }
+
+    @Test
+    public void testUsernameCommaEqualsCharactersEscaped() throws Exception {
+        String originalUsername = "user,name=";
+        String escapedUsername = "user=2Cname=3D";
+
+        String expectedInitialResponseString = "n,,n=" + escapedUsername + ",r=" + CLIENT_NONCE;
+        ProtonBuffer expectedInitialResponseBuffer = ProtonByteBufferAllocator.DEFAULT.wrap(
+            expectedInitialResponseString.getBytes(StandardCharsets.UTF_8));
+
+        ScramSHA512Mechanism mech = new ScramSHA512Mechanism(CLIENT_NONCE);
+
+        ProtonBuffer clientInitialResponse = mech.getInitialResponse(credentials(originalUsername, "password"));
+        assertEquals(expectedInitialResponseBuffer, clientInitialResponse);
+    }
+
+    @Test
+    public void testPasswordCommaEqualsCharactersNotEscaped() throws Exception {
+        Mechanism mechanism = getMechanismForTesting();
+        SaslCredentialsProvider credentials = credentials(TEST_USERNAME, TEST_PASSWORD + ",=");
+
+        ProtonBuffer clientInitialResponse = mechanism.getInitialResponse(credentials);
+        assertEquals(EXPECTED_CLIENT_INITIAL_RESPONSE, clientInitialResponse);
+
+        ProtonBuffer serverFirstMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "r=rOprNGfwEbeRWgbNEkqOf0f492bc-13cc-4050-8461-59f74f24e989,s=g2nOdJkyb5SlvqLbJb6S5+ckZpYFJ+AkJqxlmDAZYbY=,i=4096".getBytes(StandardCharsets.UTF_8));
+        ProtonBuffer expectedClientFinalMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "c=biws,r=rOprNGfwEbeRWgbNEkqOf0f492bc-13cc-4050-8461-59f74f24e989,p=vxWDY/qwIhNPGnYvGKxRESmP9nP4bmOSssNLVN6sWo1cAatr3HAxIogJ9qe2kxLdrmQcyCkW7sgq+8ybSgPphQ==".getBytes(StandardCharsets.UTF_8));
+
+        ProtonBuffer clientFinalMessage = mechanism.getChallengeResponse(credentials, serverFirstMessage);
+
+        assertEquals(expectedClientFinalMessage, clientFinalMessage);
+
+        ProtonBuffer serverFinalMessage = ProtonByteBufferAllocator.DEFAULT.wrap(
+            "v=l/icAMt3q4ym4Yh7syjjekFZ3r3L3+l+e08WmS3m3pMXCXhPf865+9bfRRprO6xPhFWKyuD+PPh+jQf8JBVojQ==".getBytes(StandardCharsets.UTF_8));
+        ProtonBuffer expectedFinalChallengeResponse = ProtonByteBufferAllocator.DEFAULT.wrap("".getBytes());
+
+        assertEquals(expectedFinalChallengeResponse, mechanism.getChallengeResponse(credentials, serverFinalMessage));
+
+        mechanism.verifyCompletion();
+    }
+}
\ No newline at end of file
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/XOauth2MechanismTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/XOauth2MechanismTest.java
new file mode 100644
index 0000000..810adbb
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/XOauth2MechanismTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.junit.jupiter.api.Test;
+
+public class XOauth2MechanismTest extends MechanismTestBase {
+
+    @Test
+    public void testGetInitialResponseWithNullUserAndPassword() throws SaslException {
+        XOauth2Mechanism mech = new XOauth2Mechanism();
+
+        ProtonBuffer response = mech.getInitialResponse(credentials());
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() != 0);
+    }
+
+    @Test
+    public void testGetChallengeResponse() throws SaslException {
+        XOauth2Mechanism mech = new XOauth2Mechanism();
+
+        ProtonBuffer response = mech.getChallengeResponse(credentials(), TEST_BUFFER);
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoCredentials() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials(null, null, false)),
+            "Should not be applicable with no credentials");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoUser() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials(null, "pass", false)),
+            "Should not be applicable with no username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoToken() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("user", null, false)),
+            "Should not be applicable with no token");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUser() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("", "pass", false)),
+            "Should not be applicable with empty username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyToken() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("user", "", false)),
+            "Should not be applicable with empty token");
+    }
+
+    /** RFC6749 defines the OAUTH2 an access token as comprising VSCHAR elements (\x20-7E) */
+    @Test
+    public void testIsNotApplicableWithIllegalAccessToken() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("user", "illegalChar\000", false)),
+            "Should not be applicable with non vschars");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUserAndToken() {
+        assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("", "", false)),
+            "Should not be applicable with empty user and token");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndToken() {
+        assertTrue(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("user", "2YotnFZFEjr1zCsicMWpAA", false)),
+            "Should be applicable with user and token");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPasswordAndPrincipal() {
+        assertTrue(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(credentials("user", "2YotnFZFEjr1zCsicMWpAA", true)),
+            "Should be applicable with user and token and principal");
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/DeliveryIdTrackerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/DeliveryIdTrackerTest.java
new file mode 100644
index 0000000..e8dc44f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/DeliveryIdTrackerTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+class DeliveryIdTrackerTest {
+
+    @Test
+    void testCreateEmptyTracker() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker();
+        assertTrue(tracker.isEmpty());
+    }
+
+    @Test
+    void testResetPutsTrackerInEmptyState() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+        assertFalse(tracker.isEmpty());
+        tracker.reset();
+        assertTrue(tracker.isEmpty());
+    }
+
+    @Test
+    void testResetAndSetNewTrackerInEmptyState() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+        assertFalse(tracker.isEmpty());
+        tracker.reset();
+        assertTrue(tracker.isEmpty());
+        tracker.set(255);
+        assertFalse(tracker.isEmpty());
+        assertEquals(255, tracker.intValue());
+        tracker.set(32767);
+        assertFalse(tracker.isEmpty());
+        assertEquals(32767, tracker.intValue());
+    }
+
+    @Test
+    void testCreateNonEmptyTracker() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+        assertFalse(tracker.isEmpty());
+        assertEquals(1, tracker.byteValue());
+        assertEquals(1, tracker.intValue());
+        assertEquals(1, tracker.longValue());
+    }
+
+    @Test
+    void testToString() {
+        assertEquals(Integer.toUnsignedString(127), new DeliveryIdTracker(127).toString());
+        assertEquals(Integer.toUnsignedString(-128), new DeliveryIdTracker(-128).toString());
+        assertEquals(Integer.toUnsignedString(0), new DeliveryIdTracker(0).toString());
+    }
+
+    @Test
+    void testHashCode() {
+        assertEquals(Integer.hashCode(127), new DeliveryIdTracker(127).hashCode());
+        assertEquals(Integer.hashCode(-128), new DeliveryIdTracker(-128).hashCode());
+        assertEquals(Integer.hashCode(0), new DeliveryIdTracker(0).hashCode());
+    }
+
+    @Test
+    void testToUnsignedInteger() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(127);
+        UnsignedInteger conversion = tracker.toUnsignedInteger();
+        assertEquals(127, conversion.byteValue());
+        assertEquals(127, conversion.intValue());
+        assertEquals(127, conversion.longValue());
+
+        assertNull(new DeliveryIdTracker().toUnsignedInteger());
+    }
+
+    @Test
+    void testGetFloatValue() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(Float.floatToIntBits(3.14f));
+        assertFalse(tracker.isEmpty());
+        assertEquals(3.14f, tracker.floatValue());
+    }
+
+    @Test
+    void testGetDoubleValue() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker((int) Double.doubleToLongBits(3.14));
+        assertFalse(tracker.isEmpty());
+        assertTrue(tracker.doubleValue() != 0);
+    }
+
+    @Test
+    void testCompareTo() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(1, tracker.compareTo(0));
+        assertEquals(0, tracker.compareTo(1));
+        assertEquals(-1, tracker.compareTo(2));
+
+        DeliveryIdTracker empty = new DeliveryIdTracker();
+        assertEquals(-1, empty.compareTo(0));
+        assertEquals(-1, empty.compareTo(1));
+        assertEquals(-1, empty.compareTo(2));
+    }
+
+    @Test
+    void testCompareToDeliveryTracker() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(1, tracker.compareTo(new DeliveryIdTracker(0)));
+        assertEquals(0, tracker.compareTo(new DeliveryIdTracker(1)));
+        assertEquals(-1, tracker.compareTo(new DeliveryIdTracker(2)));
+
+        assertEquals(-1, tracker.compareTo(new DeliveryIdTracker()));
+        assertEquals(-1, tracker.compareTo(new DeliveryIdTracker()));
+        assertEquals(-1, tracker.compareTo(new DeliveryIdTracker()));
+        assertEquals(-1, new DeliveryIdTracker().compareTo(tracker));
+    }
+
+    @Test
+    void testCompareToSequenceNumber() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(1, tracker.compareTo(new SequenceNumber(0)));
+        assertEquals(0, tracker.compareTo(new SequenceNumber(1)));
+        assertEquals(-1, tracker.compareTo(new SequenceNumber(2)));
+
+        DeliveryIdTracker empty = new DeliveryIdTracker();
+
+        assertEquals(-1, empty.compareTo(new SequenceNumber(0)));
+        assertEquals(-1, empty.compareTo(new SequenceNumber(1)));
+        assertEquals(-1, empty.compareTo(new SequenceNumber(2)));
+    }
+
+    @Test
+    void testCompareToUnsignedInteger() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(1, tracker.compareTo(new UnsignedInteger(0)));
+        assertEquals(0, tracker.compareTo(new UnsignedInteger(1)));
+        assertEquals(-1, tracker.compareTo(new UnsignedInteger(2)));
+
+        DeliveryIdTracker empty = new DeliveryIdTracker();
+
+        assertEquals(-1, empty.compareTo(new UnsignedInteger(0)));
+        assertEquals(-1, empty.compareTo(new UnsignedInteger(1)));
+        assertEquals(-1, empty.compareTo(new UnsignedInteger(2)));
+    }
+
+    @Test
+    void testEqualsToDeliveryTracker() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(new DeliveryIdTracker(1), tracker);
+        assertNotEquals(new DeliveryIdTracker(0), tracker);
+        assertNotEquals(new DeliveryIdTracker(-1), tracker);
+    }
+
+    @Test
+    void testEqualsToSequenceNumberTracker() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(tracker, new SequenceNumber(1));
+        assertNotEquals(tracker, new SequenceNumber(0));
+        assertNotEquals(tracker, new SequenceNumber(-1));
+    }
+
+    @Test
+    void testEqualsToUnsignedInteger() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertEquals(tracker, new UnsignedInteger(1));
+        assertNotEquals(tracker, new UnsignedInteger(0));
+        assertNotEquals(tracker, new UnsignedInteger(-1));
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    void testEquals() {
+        DeliveryIdTracker tracker = new DeliveryIdTracker(1);
+
+        assertFalse(tracker.equals("1"));
+        assertFalse(tracker.equals(0));
+        assertTrue(tracker.equals(1));
+        assertFalse(tracker.equals(2));
+        assertFalse(tracker.equals(new DeliveryIdTracker(0)));
+        assertTrue(tracker.equals(new DeliveryIdTracker(1)));
+        assertFalse(tracker.equals(new DeliveryIdTracker(2)));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameReadSinkTransportHandler.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameReadSinkTransportHandler.java
new file mode 100644
index 0000000..31be2d4
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameReadSinkTransportHandler.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+
+/**
+ * Drops all read frames in tests where no inbound frame handling is needed.
+ */
+public class FrameReadSinkTransportHandler implements EngineHandler {
+
+    public FrameReadSinkTransportHandler() {
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, SASLEnvelope frame) {
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope frame) {
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, HeaderEnvelope frame) {
+        context.fireWrite(frame);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope frame) {
+        context.fireWrite(frame);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope frame) {
+        context.fireWrite(frame);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameRecordingTransportHandler.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameRecordingTransportHandler.java
new file mode 100644
index 0000000..4af510b
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameRecordingTransportHandler.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.PerformativeEnvelope;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+
+public class FrameRecordingTransportHandler implements EngineHandler {
+
+    private List<PerformativeEnvelope<?>> framesRead = new ArrayList<>();
+    private List<PerformativeEnvelope<?>> framesWritten = new ArrayList<>();
+
+    public FrameRecordingTransportHandler() {
+    }
+
+    public List<PerformativeEnvelope<?>> getFramesWritten() {
+        return framesWritten;
+    }
+
+    public List<PerformativeEnvelope<?>> getFramesRead() {
+        return framesRead;
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+        framesRead.add(header);
+        context.fireRead(header);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, SASLEnvelope frame) {
+        framesRead.add(frame);
+        context.fireRead(frame);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope frame) {
+        framesRead.add(frame);
+        context.fireRead(frame);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, HeaderEnvelope frame) {
+        framesWritten.add(frame);
+        context.fireWrite(frame);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope frame) {
+        framesWritten.add(frame);
+        context.fireWrite(frame);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope frame) {
+        framesWritten.add(frame);
+        context.fireWrite(frame);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameWriteSinkTransportHandler.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameWriteSinkTransportHandler.java
new file mode 100644
index 0000000..fc77a70
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/FrameWriteSinkTransportHandler.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import org.apache.qpid.protonj2.engine.EngineHandler;
+import org.apache.qpid.protonj2.engine.EngineHandlerContext;
+import org.apache.qpid.protonj2.engine.HeaderEnvelope;
+import org.apache.qpid.protonj2.engine.IncomingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.OutgoingAMQPEnvelope;
+import org.apache.qpid.protonj2.engine.SASLEnvelope;
+
+/**
+ * All writes are dropped to prevent errors in tests where no outbound capture happens.
+ */
+public class FrameWriteSinkTransportHandler implements EngineHandler {
+
+    public FrameWriteSinkTransportHandler() {
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, HeaderEnvelope header) {
+        context.fireRead(header);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, SASLEnvelope frame) {
+        context.fireRead(frame);
+    }
+
+    @Override
+    public void handleRead(EngineHandlerContext context, IncomingAMQPEnvelope frame) {
+        context.fireRead(frame);
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, HeaderEnvelope frame) {
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, SASLEnvelope frame) {
+    }
+
+    @Override
+    public void handleWrite(EngineHandlerContext context, OutgoingAMQPEnvelope frame) {
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/LinkedSplayMapTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/LinkedSplayMapTest.java
new file mode 100644
index 0000000..5e59d3e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/LinkedSplayMapTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test LinkedSplayMap type
+ */
+public class LinkedSplayMapTest extends SplayMapTest {
+
+    protected static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(SplayMapTest.class);
+
+    @Override
+    @BeforeEach
+    public void setUp() {
+        super.setUp();
+    }
+
+    @Override
+    protected <E> LinkedSplayMap<E> createMap() {
+        return new LinkedSplayMap<>();
+    }
+
+    /**
+     * Test differs from parent as order is insertion based.
+     */
+    @Override
+    @Test
+    public void testValuesIterationFollowUnsignedOrderingExpectations() {
+        LinkedSplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<String> values = map.values();
+        Iterator<String> iterator = values.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals("" + expectedOrder[counter++], iterator.next());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(inputValues.length, counter);
+    }
+
+    /**
+     * Test differs from parent as order is insertion based.
+     */
+    @Override
+    @Test
+    public void testKeysIterationFollowsUnsignedOrderingExpectations() {
+        LinkedSplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<UnsignedInteger> keys = map.keySet();
+        Iterator<UnsignedInteger> iterator = keys.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals(UnsignedInteger.valueOf(expectedOrder[counter++]), iterator.next());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(inputValues.length, counter);
+    }
+
+    /**
+     * Test differs from parent as order is insertion based.
+     */
+    @Override
+    @Test
+    public void testEntryIterationFollowsUnsignedOrderingExpectations() {
+        LinkedSplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        Iterator<Entry<UnsignedInteger, String>> iterator = entries.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            Entry<UnsignedInteger, String> entry = iterator.next();
+            assertNotNull(entry);
+            assertEquals(UnsignedInteger.valueOf(expectedOrder[counter]), entry.getKey());
+            assertEquals("" + expectedOrder[counter++], entry.getValue());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(inputValues.length, counter);
+    }
+
+    /**
+     * Test differs from parent as order is insertion based.
+     */
+    @Override
+    @Test
+    public void testForEach() {
+        LinkedSplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        final SequenceNumber index = new SequenceNumber(0);
+        map.forEach((k, v) -> {
+            int value = index.getAndIncrement().intValue();
+            assertEquals(expectedOrder[value], k.intValue());
+        });
+
+        assertEquals(index.intValue(), inputValues.length);
+    }
+
+    /**
+     * Test differs from parent as order is insertion based.
+     */
+    @Override
+    @Test
+    public void testForEachEntry() {
+        LinkedSplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        final SequenceNumber index = new SequenceNumber(0);
+        map.forEach((value) -> {
+            int i = index.getAndIncrement().intValue();
+            assertEquals(expectedOrder[i] + "", value);
+        });
+
+        assertEquals(index.intValue(), inputValues.length);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/RingQueueTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/RingQueueTest.java
new file mode 100644
index 0000000..6f8f06c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/RingQueueTest.java
@@ -0,0 +1,410 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.Random;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class RingQueueTest {
+
+    protected long seed;
+    protected Random random;
+
+    @BeforeEach
+    public void setUp() {
+        seed = System.currentTimeMillis();
+        random = new Random();
+        random.setSeed(seed);
+    }
+
+    @Test
+    public void testCreate() {
+        Queue<String> testQ = new RingQueue<>(16);
+
+        assertTrue(testQ.isEmpty());
+        assertEquals(0, testQ.size());
+        assertNull(testQ.peek());
+    }
+
+    @Test
+    public void testOffer() {
+        Queue<String> testQ = new RingQueue<>(3);
+
+        testQ.offer("1");
+        testQ.offer("2");
+        testQ.offer("3");
+
+        assertFalse(testQ.isEmpty());
+        assertEquals(3, testQ.size());
+        assertNotNull(testQ.peek());
+        assertEquals("1", testQ.peek());
+    }
+
+    @Test
+    public void testRemoveAll() {
+        Queue<String> testQ = new RingQueue<>(3);
+        Queue<String> inputQ = new ArrayDeque<>(Arrays.asList("1", "2", "3"));
+        assertThrows(UnsupportedOperationException.class, () -> testQ.removeAll(inputQ));
+    }
+
+    @Test
+    public void testRetainAll() {
+        Queue<String> testQ = new RingQueue<>(3);
+        Queue<String> inputQ = new ArrayDeque<>(Arrays.asList("1", "2", "3"));
+        assertThrows(UnsupportedOperationException.class, () -> testQ.retainAll(inputQ));
+    }
+
+    @Test
+    public void testAddAllWithSelf() {
+        Queue<String> testQ = new RingQueue<>(3);
+        assertThrows(IllegalArgumentException.class, () -> testQ.addAll(testQ));
+    }
+
+    @Test
+    public void testAddAllFromLargerCollection() {
+        Queue<String> testQ = new RingQueue<>(2);
+        Queue<String> inputQ = new ArrayDeque<>(Arrays.asList("1", "2", "3"));
+
+        assertThrows(IllegalStateException.class, () -> testQ.addAll(inputQ));
+    }
+
+    @Test
+    public void testAddAllFromSmallerCollection() {
+        Queue<String> testQ = new RingQueue<>(4);
+        Queue<String> inputQ = new ArrayDeque<>(Arrays.asList("1", "2", "3"));
+
+        assertTrue(testQ.addAll(inputQ));
+
+        assertEquals(inputQ.size(), testQ.size());
+        inputQ.forEach((element) -> {
+            assertEquals(element, testQ.poll());
+        });
+    }
+
+    @Test
+    public void testAddAllFromSameSizeCollection() {
+        Queue<String> testQ = new RingQueue<>(3);
+        Queue<String> inputQ = new ArrayDeque<>(Arrays.asList("1", "2", "3"));
+
+        assertTrue(testQ.addAll(inputQ));
+
+        assertEquals(inputQ.size(), testQ.size());
+        inputQ.forEach((element) -> {
+            assertEquals(element, testQ.poll());
+        });
+    }
+
+    @Test
+    public void testAddAllFromEmptyCollection() {
+        Queue<String> testQ = new RingQueue<>(3);
+        Queue<String> inputQ = new ArrayDeque<>();
+
+        assertFalse(testQ.addAll(inputQ));
+    }
+
+    @Test
+    public void testRemove() {
+        Queue<String> testQ = new RingQueue<>(3);
+
+        testQ.offer("1");
+        testQ.offer("2");
+        testQ.offer("3");
+
+        assertThrows(UnsupportedOperationException.class, () -> testQ.remove("1"));
+    }
+
+    @Test
+    public void testPoll() {
+        Queue<String> testQ = new RingQueue<>(3);
+
+        testQ.offer("1");
+        testQ.offer("2");
+        testQ.offer("3");
+
+        assertFalse(testQ.isEmpty());
+        assertEquals(3, testQ.size());
+        assertNotNull(testQ.peek());
+        assertEquals("1", testQ.peek());
+
+        assertEquals("1", testQ.poll());
+        assertEquals("2", testQ.poll());
+        assertEquals("3", testQ.poll());
+
+        assertTrue(testQ.isEmpty());
+        assertEquals(0, testQ.size());
+        assertNull(testQ.peek());
+    }
+
+    @Test
+    public void testOfferAfterDequeueFromFull() {
+        Queue<String> testQ = new RingQueue<>(3);
+
+        testQ.offer("1");
+        testQ.offer("2");
+        testQ.offer("3");
+
+        assertFalse(testQ.isEmpty());
+        assertEquals(3, testQ.size());
+        assertNotNull(testQ.peek());
+        assertEquals("1", testQ.peek());
+        assertEquals("1", testQ.poll());
+        assertFalse(testQ.isEmpty());
+        assertEquals(2, testQ.size());
+        assertNotNull(testQ.peek());
+        assertEquals("2", testQ.peek());
+
+        testQ.offer("4");
+
+        assertEquals("2", testQ.poll());
+        assertEquals("3", testQ.poll());
+        assertEquals("4", testQ.poll());
+
+        assertTrue(testQ.isEmpty());
+        assertEquals(0, testQ.size());
+        assertNull(testQ.peek());
+    }
+
+    @Test
+    public void testIterateOverFullQueue() {
+        Queue<String> testQ = new RingQueue<>(3);
+
+        Queue<String> inputQ = new ArrayDeque<>(Arrays.asList("1", "2", "3"));
+
+        inputQ.forEach(value -> testQ.offer(value));
+
+        assertFalse(testQ.isEmpty());
+        assertEquals(3, testQ.size());
+        assertNotNull(testQ.peek());
+
+        Iterator<String> iter = testQ.iterator();
+        assertTrue(iter.hasNext());
+
+        while (iter.hasNext()) {
+            String next = iter.next();
+            assertEquals(inputQ.poll(), next);
+        }
+    }
+
+    @Test
+    public void testContains() {
+        final int COUNT = 100;
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.contains("" + random.nextInt()));
+        }
+
+        assertFalse(testQ.contains("this-string"));
+        assertFalse(testQ.contains(null));
+    }
+
+    @Test
+    public void testContainsNullElement() {
+        Queue<String> testQ = new RingQueue<>(10);
+
+        testQ.offer("1");
+        testQ.offer(null);
+        testQ.offer("2");
+        testQ.offer(null);
+        testQ.offer("3");
+
+        assertTrue(testQ.contains("1"));
+        assertTrue(testQ.contains(null));
+
+        assertEquals("1", testQ.poll());
+        assertEquals(null, testQ.poll());
+
+        assertTrue(testQ.contains("2"));
+        assertTrue(testQ.contains(null));
+    }
+
+    @Test
+    public void testIterateOverQueue() {
+        final int COUNT = 100;
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        testQ.forEach(entry -> assertEquals(entry, String.valueOf(random.nextInt())));
+
+        random.setSeed(seed);  // Reset
+
+        for (int i = 0; i < COUNT / 2; ++i) {
+            assertEquals(String.valueOf(random.nextInt()), testQ.poll());
+        }
+
+        testQ.forEach(entry -> assertEquals(entry, String.valueOf(random.nextInt())));
+    }
+
+    @Test
+    public void testIterateOverQueueFilledViaCollection() {
+        final int COUNT = 100;
+        Queue<String> inputQ = new ArrayDeque<>();
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(inputQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        Queue<String> testQ = new RingQueue<>(inputQ);
+
+        testQ.forEach(entry -> assertEquals(entry, String.valueOf(random.nextInt())));
+
+        random.setSeed(seed);  // Reset
+
+        for (int i = 0; i < COUNT / 2; ++i) {
+            assertEquals(String.valueOf(random.nextInt()), testQ.poll());
+        }
+
+        testQ.forEach(entry -> assertEquals(entry, String.valueOf(random.nextInt())));
+    }
+
+    @Test
+    public void testOfferPollAndOffer() {
+        final int ITERATIONS = 10;
+        final int COUNT = 100;
+
+        final List<String> dataSet = new ArrayList<>(COUNT);
+        for (int i = 0; i < COUNT; ++i) {
+            dataSet.add("" + random.nextInt());
+        }
+
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int iteration = 0; iteration < ITERATIONS; ++iteration) {
+            testQ.clear();
+
+            for (int i = 0; i < COUNT; ++i) {
+                assertTrue(testQ.offer(dataSet.get(i)));
+            }
+
+            assertFalse(testQ.isEmpty());
+
+            for (int i = 0; i < COUNT; ++i) {
+                assertNotNull(testQ.poll());
+            }
+
+            assertTrue(testQ.isEmpty());
+
+            for (int i = 0; i < COUNT; ++i) {
+                assertTrue(testQ.offer(dataSet.get(i)));
+            }
+
+            assertFalse(testQ.isEmpty());
+
+            for (int i = 0; i < COUNT; ++i) {
+                assertTrue(testQ.contains(dataSet.get(0)));
+            }
+        }
+    }
+
+    @Test
+    public void testIterateOverQueueThrowsNoSuchElementIfMovedToFar() {
+        final int COUNT = 100;
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        Iterator<String> iterator = testQ.iterator();
+        while (iterator.hasNext()) {
+            assertEquals(String.valueOf(random.nextInt()), iterator.next());
+        }
+
+        assertThrows(NoSuchElementException.class, () -> iterator.next());
+    }
+
+    @Test
+    public void testIteratorThrowsIfModifiedConcurrently() {
+        final int COUNT = 100;
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        Iterator<String> iterator = testQ.iterator();
+        assertEquals(testQ.poll(), "" + random.nextInt());
+        assertThrows(ConcurrentModificationException.class, () -> iterator.next());
+    }
+
+    @Test
+    public void testIteratorThrowsIfModifiedConcurrentlySizeUnchanged() {
+        final int COUNT = 100;
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        Iterator<String> iterator = testQ.iterator();
+        assertEquals(testQ.poll(), "" + random.nextInt());
+        assertTrue(testQ.offer("" + random.nextInt()));
+        assertThrows(ConcurrentModificationException.class, () -> iterator.next());
+    }
+
+    @Test
+    public void testIteratorDoesNotSupportRemove() {
+        final int COUNT = 100;
+        Queue<String> testQ = new RingQueue<>(COUNT);
+
+        for (int i = 0; i < COUNT; ++i) {
+            assertTrue(testQ.offer("" + random.nextInt()));
+        }
+
+        random.setSeed(seed);  // Reset
+
+        Iterator<String> iterator = testQ.iterator();
+        assertTrue(iterator.hasNext());
+        assertThrows(UnsupportedOperationException.class, () -> iterator.remove());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SequenceNumberTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SequenceNumberTest.java
new file mode 100644
index 0000000..35b0e45
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SequenceNumberTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+class SequenceNumberTest {
+
+    @Test
+    void testGetFloatValue() {
+        SequenceNumber number = new SequenceNumber(Float.floatToIntBits(3.14f));
+        assertEquals(3.14f, number.floatValue());
+    }
+
+    @Test
+    void testGetDoubleValue() {
+        SequenceNumber number = new SequenceNumber((int) Double.doubleToLongBits(3.14));
+        assertTrue(number.doubleValue() != 0);
+    }
+
+    @Test
+    void testCreateSequenceNumber() {
+        SequenceNumber number = new SequenceNumber(1);
+        assertTrue(number.equals(1));
+        assertEquals(1, number.intValue());
+    }
+
+    @Test
+    void testIncrement() {
+        SequenceNumber number = new SequenceNumber(1);
+        assertTrue(number.equals(1));
+        assertEquals(new SequenceNumber(2), number.increment());
+    }
+
+    @Test
+    void testDecrement() {
+        SequenceNumber number = new SequenceNumber(1);
+        assertTrue(number.equals(1));
+        assertEquals(new SequenceNumber(0), number.decrement());
+    }
+
+    @Test
+    void testGetAndIncrement() {
+        SequenceNumber number = new SequenceNumber(1);
+        assertTrue(number.equals(1));
+        assertEquals(new SequenceNumber(1), number.getAndIncrement());
+        assertTrue(number.equals(2));
+    }
+
+    @Test
+    void testGetAndDecrement() {
+        SequenceNumber number = new SequenceNumber(1);
+        assertTrue(number.equals(1));
+        assertEquals(new SequenceNumber(1), number.getAndDecrement());
+        assertTrue(number.equals(0));
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    void testEquals() {
+        SequenceNumber number = new SequenceNumber(1);
+
+        assertFalse(number.equals("1"));
+        assertFalse(number.equals(0));
+        assertTrue(number.equals(1));
+        assertFalse(number.equals(2));
+        assertFalse(number.equals(new SequenceNumber(0)));
+        assertTrue(number.equals(new SequenceNumber(1)));
+        assertFalse(number.equals(new SequenceNumber(2)));
+    }
+
+    @Test
+    void testCompareToInt() {
+        SequenceNumber number = new SequenceNumber(1);
+
+        assertEquals(1, number.compareTo(0));
+        assertEquals(0, number.compareTo(1));
+        assertEquals(-1, number.compareTo(2));
+    }
+
+    @Test
+    void testCompareToSequenceNumber() {
+        SequenceNumber number = new SequenceNumber(1);
+
+        assertEquals(1, number.compareTo(new SequenceNumber(0)));
+        assertEquals(0, number.compareTo(new SequenceNumber(1)));
+        assertEquals(-1, number.compareTo(new SequenceNumber(2)));
+    }
+
+    @Test
+    void testCompareToUnsignedInteger() {
+        SequenceNumber number = new SequenceNumber(1);
+
+        assertEquals(1, number.compareTo(new UnsignedInteger(0)));
+        assertEquals(0, number.compareTo(new UnsignedInteger(1)));
+        assertEquals(-1, number.compareTo(new UnsignedInteger(2)));
+    }
+
+    @Test
+    void testToString() {
+        assertEquals(Integer.toUnsignedString(127), new SequenceNumber(127).toString());
+        assertEquals(Integer.toUnsignedString(-128), new SequenceNumber(-128).toString());
+        assertEquals(Integer.toUnsignedString(0), new SequenceNumber(0).toString());
+    }
+
+    @Test
+    void testHashCode() {
+        assertEquals(Integer.hashCode(127), new SequenceNumber(127).hashCode());
+        assertEquals(Integer.hashCode(-128), new SequenceNumber(-128).hashCode());
+        assertEquals(Integer.hashCode(0), new SequenceNumber(0).hashCode());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SimplePojo.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SimplePojo.java
new file mode 100644
index 0000000..d2e5e4d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SimplePojo.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import java.io.Serializable;
+
+public class SimplePojo implements Serializable {
+
+    private static final long serialVersionUID = 3258560248864895099L;
+
+    private Object payload;
+
+    public SimplePojo() {
+    }
+
+    public SimplePojo(Object payload) {
+        this.payload = payload;
+    }
+
+    public Object getPayload() {
+        return payload;
+    }
+
+    public void setPayload(Object payload) {
+        this.payload = payload;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((payload == null) ? 0 : payload.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        SimplePojo other = (SimplePojo) obj;
+        if (payload == null) {
+            if (other.payload != null) {
+                return false;
+            }
+        } else if (!payload.equals(other.payload)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SplayMapTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SplayMapTest.java
new file mode 100644
index 0000000..edb11b3
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/util/SplayMapTest.java
@@ -0,0 +1,1451 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.Set;
+
+import org.apache.qpid.protonj2.logging.ProtonLogger;
+import org.apache.qpid.protonj2.logging.ProtonLoggerFactory;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test {@link SplayMap} type
+ */
+public class SplayMapTest {
+
+    protected static final ProtonLogger LOG = ProtonLoggerFactory.getLogger(SplayMapTest.class);
+
+    protected long seed;
+    protected Random random;
+
+    @BeforeEach
+    public void setUp() {
+        seed = System.nanoTime();
+        random = new Random();
+        random.setSeed(seed);
+    }
+
+    protected <E> SplayMap<E> createMap() {
+        return new SplayMap<>();
+    }
+
+    @Test
+    public void testComparator() {
+        SplayMap<String> map = createMap();
+
+        assertNotNull(map.comparator());
+        assertSame(map.comparator(), map.comparator());
+    }
+
+    @Test
+    public void testClear() {
+        SplayMap<String> map = createMap();
+
+        assertEquals(0, map.size());
+        assertTrue(map.isEmpty());
+
+        map.put(2, "two");
+        map.put(0, "zero");
+        map.put(1, "one");
+
+        assertEquals(3, map.size());
+        assertFalse(map.isEmpty());
+
+        map.clear();
+
+        assertEquals(0, map.size());
+        assertTrue(map.isEmpty());
+
+        map.put(5, "five");
+        map.put(9, "nine");
+        map.put(3, "three");
+        map.put(7, "seven");
+        map.put(-1, "minus one");
+
+        assertEquals(5, map.size());
+        assertFalse(map.isEmpty());
+
+        map.clear();
+
+        assertEquals(0, map.size());
+        assertTrue(map.isEmpty());
+
+        map.clear();
+    }
+
+    @Test
+    public void testSize() {
+        SplayMap<String> map = createMap();
+
+        assertEquals(0, map.size());
+        map.put(0, "zero");
+        assertEquals(1, map.size());
+        map.put(1, "one");
+        assertEquals(2, map.size());
+        map.put(0, "update");
+        assertEquals(2, map.size());
+        map.remove(0);
+        assertEquals(1, map.size());
+        map.remove(1);
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testInsert() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "two");
+        map.put(3, "three");
+        map.put(5, "five");
+        map.put(9, "nine");
+        map.put(7, "seven");
+        map.put(-1, "minus one");
+
+        assertEquals(8, map.size());
+    }
+
+    @Test
+    public void testInsertUnsignedInteger() {
+        SplayMap<String> map = createMap();
+
+        map.put(UnsignedInteger.valueOf(0), "zero");
+        map.put(UnsignedInteger.valueOf(1), "one");
+        map.put(UnsignedInteger.valueOf(2), "two");
+        map.put(UnsignedInteger.valueOf(3), "three");
+        map.put(UnsignedInteger.valueOf(5), "five");
+        map.put(UnsignedInteger.valueOf(9), "nine");
+        map.put(UnsignedInteger.valueOf(7), "seven");
+        map.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        assertEquals(8, map.size());
+    }
+
+    @Test
+    public void testInsertAndReplace() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "foo");
+        assertEquals("foo", map.put(2, "two"));
+
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("two", map.get(2));
+
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testInsertAndRemove() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "two");
+
+        assertEquals(3, map.size());
+
+        assertEquals("zero", map.remove(0));
+        assertEquals("one", map.remove(1));
+        assertEquals("two", map.remove(2));
+
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testPutAll() {
+        SplayMap<String> map = createMap();
+
+        Map<UnsignedInteger, String> hashmap = new HashMap<>();
+
+        hashmap.put(UnsignedInteger.valueOf(0), "zero");
+        hashmap.put(UnsignedInteger.valueOf(1), "one");
+        hashmap.put(UnsignedInteger.valueOf(2), "two");
+        hashmap.put(UnsignedInteger.valueOf(3), "three");
+        hashmap.put(UnsignedInteger.valueOf(5), "five");
+        hashmap.put(UnsignedInteger.valueOf(9), "nine");
+        hashmap.put(UnsignedInteger.valueOf(7), "seven");
+        hashmap.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        map.putAll(hashmap);
+
+        assertEquals(8, map.size());
+
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("two", map.get(2));
+        assertEquals("three", map.get(3));
+        assertEquals("five", map.get(5));
+        assertEquals("nine", map.get(9));
+        assertEquals("seven", map.get(7));
+        assertEquals("minus one", map.get(-1));
+    }
+
+    @Test
+    public void testPutIfAbsent() {
+        SplayMap<String> map = createMap();
+
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(0), "zero"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(1), "one"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(2), "two"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(3), "three"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(5), "five"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(9), "nine"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(7), "seven"));
+        assertNull(map.putIfAbsent(UnsignedInteger.valueOf(-1), "minus one"));
+
+        assertEquals(8, map.size());
+
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("two", map.get(2));
+        assertEquals("three", map.get(3));
+        assertEquals("five", map.get(5));
+        assertEquals("nine", map.get(9));
+        assertEquals("seven", map.get(7));
+        assertEquals("minus one", map.get(-1));
+
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(0), "zero-zero"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(1), "one-one"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(2), "two-two"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(3), "three-three"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(5), "five-five"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(9), "nine-nine"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(7), "seven-seven"));
+        assertNotNull(map.putIfAbsent(UnsignedInteger.valueOf(-1), "minus one minus one"));
+
+        assertEquals(8, map.size());
+
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("two", map.get(2));
+        assertEquals("three", map.get(3));
+        assertEquals("five", map.get(5));
+        assertEquals("nine", map.get(9));
+        assertEquals("seven", map.get(7));
+        assertEquals("minus one", map.get(-1));
+    }
+
+    @Test
+    public void testGetWhenEmpty() {
+        SplayMap<String> map = createMap();
+
+        assertNull(map.get(0));
+    }
+
+    @Test
+    public void testGet() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(-3, "-three");
+
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("-three", map.get(-3));
+
+        assertNull(map.get(3));
+
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testGetUnsignedInteger() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(-3, "-three");
+
+        assertEquals("zero", map.get(UnsignedInteger.valueOf(0)));
+        assertEquals("one", map.get(UnsignedInteger.valueOf(1)));
+        assertEquals("-three", map.get(UnsignedInteger.valueOf(-3)));
+
+        assertNull(map.get(3));
+
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testContainsKeyOnEmptyMap() {
+        SplayMap<String> map = createMap();
+
+        assertFalse(map.containsKey(0));
+        assertFalse(map.containsKey(UnsignedInteger.ZERO));
+    }
+
+    @Test
+    public void testContainsKey() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(-3, "-three");
+
+        assertTrue(map.containsKey(0));
+        assertFalse(map.containsKey(3));
+
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testContainsKeyUnsignedInteger() {
+        SplayMap<String> map = createMap();
+
+        map.put(UnsignedInteger.valueOf(0), "zero");
+        map.put(UnsignedInteger.valueOf(1), "one");
+        map.put(UnsignedInteger.valueOf(-3), "-three");
+
+        assertTrue(map.containsKey(0));
+        assertFalse(map.containsKey(3));
+
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testContainsValue() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(-3, "-three");
+
+        assertTrue(map.containsValue("zero"));
+        assertFalse(map.containsValue("four"));
+
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testContainsValueOnEmptyMap() {
+        SplayMap<String> map = createMap();
+
+        assertFalse(map.containsValue("0"));
+    }
+
+    @Test
+    public void testRemove() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(9, "nine");
+        map.put(7, "seven");
+        map.put(-1, "minus one");
+
+        assertEquals(5, map.size());
+        assertNull(map.remove(5));
+        assertEquals(5, map.size());
+        assertEquals("nine", map.remove(9));
+        assertEquals(4, map.size());
+    }
+
+    @Test
+    public void testRemoveIsIdempotent() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "two");
+
+        assertEquals(3, map.size());
+
+        assertEquals("zero", map.remove(0));
+        assertEquals(null, map.remove(0));
+
+        assertEquals(2, map.size());
+
+        assertEquals("one", map.remove(1));
+        assertEquals(null, map.remove(1));
+
+        assertEquals(1, map.size());
+
+        assertEquals("two", map.remove(2));
+        assertEquals(null, map.remove(2));
+
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testRemoveValueNotInMap() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(9, "nine");
+        map.put(7, "seven");
+        map.put(-1, "minus one");
+
+        assertNull(map.remove(5));
+    }
+
+    @Test
+    public void testRemoveFirstEntryTwice() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(16, "sixteen");
+
+        assertNotNull(map.remove(0));
+        assertNull(map.remove(0));
+    }
+
+    @Test
+    public void testRemoveWithInvalidType() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+
+        try {
+            map.remove("foo");
+            fail("Should not accept incompatible types");
+        } catch (ClassCastException ccex) {}
+    }
+
+    @Test
+    public void testRemoveUnsignedInteger() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(UnsignedInteger.valueOf(9), "nine");
+        map.put(7, "seven");
+        map.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        assertEquals(5, map.size());
+        assertNull(map.remove(UnsignedInteger.valueOf(5)));
+        assertEquals(5, map.size());
+        assertEquals("nine", map.remove(UnsignedInteger.valueOf(9)));
+        assertEquals(4, map.size());
+    }
+
+    @Test
+    public void testRemoveInteger() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(UnsignedInteger.valueOf(9), "nine");
+        map.put(7, "seven");
+        map.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        assertEquals(5, map.size());
+        assertNull(map.remove(Integer.valueOf(5)));
+        assertEquals(5, map.size());
+        assertEquals("nine", map.remove(Integer.valueOf(9)));
+        assertEquals(4, map.size());
+    }
+
+    @Test
+    public void testRemoveEntryWithValue() {
+        SplayMap<String> map = createMap();
+
+        assertFalse(map.remove(1, "zero"));
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(UnsignedInteger.valueOf(9), "nine");
+        map.put(7, "seven");
+        map.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        assertEquals(5, map.size());
+        assertFalse(map.remove(1, "zero"));
+        assertEquals(5, map.size());
+        assertTrue(map.remove(1, "one"));
+        assertEquals(4, map.size());
+        assertFalse(map.remove(42, "forty-two"));
+        assertEquals(4, map.size());
+
+        assertEquals("zero", map.get(0));
+        assertEquals("nine", map.get(UnsignedInteger.valueOf(9)));
+        assertEquals("seven", map.get(7));
+        assertEquals("minus one", map.get(-1));
+    }
+
+    @Test
+    public void testReplaceOldValueWithNew() {
+        SplayMap<String> map = createMap();
+
+        assertFalse(map.replace(1, "two", "zero-zero"));
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(UnsignedInteger.valueOf(9), "nine");
+        map.put(7, "seven");
+        map.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        assertEquals(5, map.size());
+        assertFalse(map.replace(1, "two", "zero-zero"));
+        assertEquals(5, map.size());
+        assertTrue(map.replace(1, "one", "one-one"));
+        assertEquals(5, map.size());
+        assertFalse(map.replace(42, null, "forty-two"));
+        assertEquals(5, map.size());
+        assertEquals("one-one", map.get(1));
+
+        assertTrue(map.replace(UnsignedInteger.valueOf(1), "one-one", "one"));
+        assertEquals(5, map.size());
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("nine", map.get(UnsignedInteger.valueOf(9)));
+        assertEquals("seven", map.get(7));
+        assertEquals("minus one", map.get(-1));    }
+
+    @Test
+    public void testReplaceValue() {
+        SplayMap<String> map = createMap();
+
+        assertNull(map.replace(1, "zero-zero"));
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(UnsignedInteger.valueOf(9), "nine");
+        map.put(7, "seven");
+        map.put(UnsignedInteger.valueOf(-1), "minus one");
+
+        assertEquals(5, map.size());
+        assertEquals("one", map.replace(1, "one-one"));
+        assertEquals(5, map.size());
+        assertNull(map.replace(42, "forty-two"));
+        assertEquals(5, map.size());
+        assertEquals("one-one", map.get(1));
+
+        assertEquals("one-one", map.replace(UnsignedInteger.valueOf(1), "one"));
+        assertEquals(5, map.size());
+        assertEquals("zero", map.get(0));
+        assertEquals("one", map.get(1));
+        assertEquals("nine", map.get(UnsignedInteger.valueOf(9)));
+        assertEquals("seven", map.get(7));
+        assertEquals("minus one", map.get(-1));
+    }
+
+    @Test
+    public void testValuesCollection() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "one");
+        map.put(3, "one");
+
+        Collection<String> values = map.values();
+        assertNotNull(values);
+        assertEquals(4, values.size());
+        assertFalse(values.isEmpty());
+        assertSame(values, map.values());
+    }
+
+    @Test
+    public void testValuesIteration() {
+        SplayMap<String> map = createMap();
+
+        final int[] intValues = {0, 1, 2, 3};
+
+        for (int entry : intValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<String> values = map.values();
+        Iterator<String> iterator = values.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals("" + intValues[counter++], iterator.next());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(intValues.length, counter);
+    }
+
+    @Test
+    public void testValuesIterationRemove() {
+        SplayMap<String> map = createMap();
+
+        final int[] intValues = {0, 1, 2, 3};
+
+        for (int entry : intValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<String> values = map.values();
+        Iterator<String> iterator = values.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals("" + intValues[counter++], iterator.next());
+            iterator.remove();
+        }
+
+        // Check that we really did iterate.
+        assertEquals(intValues.length, counter);
+        assertTrue(map.isEmpty());
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testValuesIterationFollowUnsignedOrderingExpectations() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<String> values = map.values();
+        Iterator<String> iterator = values.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals("" + expectedOrder[counter++], iterator.next());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(inputValues.length, counter);
+    }
+
+    @Test
+    public void testValuesIterationFailsWhenConcurrentlyModified() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<String> values = map.values();
+        Iterator<String> iterator = values.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        map.remove(3);
+
+        try {
+            iterator.next();
+            fail("Should not iterate when modified outside of iterator");
+        } catch (ConcurrentModificationException cme) {}
+    }
+
+    @Test
+    public void testValuesIterationOnEmptyTree() {
+        SplayMap<String> map = createMap();
+        Collection<String> values = map.values();
+        Iterator<String> iterator = values.iterator();
+
+        assertFalse(iterator.hasNext());
+        try {
+            iterator.next();
+            fail("Should have thrown a NoSuchElementException");
+        } catch (NoSuchElementException nse) {
+        }
+    }
+
+    @Test
+    public void testKeySetReturned() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "two");
+        map.put(3, "three");
+
+        Set<UnsignedInteger> keys = map.keySet();
+        assertNotNull(keys);
+        assertEquals(4, keys.size());
+        assertFalse(keys.isEmpty());
+        assertSame(keys, map.keySet());
+    }
+
+    @Test
+    public void testKeysIterationRemove() {
+        SplayMap<String> map = createMap();
+
+        final int[] intValues = {0, 1, 2, 3};
+
+        for (int entry : intValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<UnsignedInteger> keys = map.keySet();
+        Iterator<UnsignedInteger> iterator = keys.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals(UnsignedInteger.valueOf(intValues[counter++]), iterator.next());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(intValues.length, counter);
+    }
+
+    @Test
+    public void testKeysIteration() {
+        SplayMap<String> map = createMap();
+
+        final int[] intValues = {0, 1, 2, 3};
+
+        for (int entry : intValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<UnsignedInteger> keys = map.keySet();
+        Iterator<UnsignedInteger> iterator = keys.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals(UnsignedInteger.valueOf(intValues[counter++]), iterator.next());
+            iterator.remove();
+        }
+
+        // Check that we really did iterate.
+        assertEquals(intValues.length, counter);
+        assertTrue(map.isEmpty());
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testKeysIterationFollowsUnsignedOrderingExpectations() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<UnsignedInteger> keys = map.keySet();
+        Iterator<UnsignedInteger> iterator = keys.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            assertEquals(UnsignedInteger.valueOf(expectedOrder[counter++]), iterator.next());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(inputValues.length, counter);
+    }
+
+    @Test
+    public void testKeysIterationFailsWhenConcurrentlyModified() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Collection<UnsignedInteger> keys = map.keySet();
+        Iterator<UnsignedInteger> iterator = keys.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        map.remove(3);
+
+        try {
+            iterator.next();
+            fail("Should not iterate when modified outside of iterator");
+        } catch (ConcurrentModificationException cme) {}
+    }
+
+    @Test
+    public void testKeysIterationOnEmptyTree() {
+        SplayMap<String> map = createMap();
+        Collection<UnsignedInteger> keys = map.keySet();
+        Iterator<UnsignedInteger> iterator = keys.iterator();
+
+        assertFalse(iterator.hasNext());
+        try {
+            iterator.next();
+            fail("Should have thrown a NoSuchElementException");
+        } catch (NoSuchElementException nse) {
+        }
+    }
+
+    @Test
+    public void tesEntrySetReturned() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "two");
+        map.put(3, "three");
+
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        assertNotNull(entries);
+        assertEquals(4, entries.size());
+        assertFalse(entries.isEmpty());
+        assertSame(entries, map.entrySet());
+    }
+
+    @Test
+    public void tesEntrySetContains() {
+        SplayMap<String> map = createMap();
+
+        map.put(0, "zero");
+        map.put(1, "one");
+        map.put(2, "two");
+        map.put(3, "three");
+
+        Set<Entry<UnsignedInteger, String>> entries = map.entrySet();
+        assertNotNull(entries);
+        assertEquals(4, entries.size());
+        assertFalse(entries.isEmpty());
+        assertSame(entries, map.entrySet());
+
+        OutsideEntry<UnsignedInteger, String> entry1 = new OutsideEntry<>(UnsignedInteger.valueOf(0), "zero");
+        OutsideEntry<UnsignedInteger, String> entry2 = new OutsideEntry<>(UnsignedInteger.valueOf(0), "hero");
+
+        assertTrue(entries.contains(entry1));
+        assertFalse(entries.contains(entry2));
+    }
+
+    @Test
+    public void testEntryIteration() {
+        SplayMap<String> map = createMap();
+
+        final int[] intValues = {0, 1, 2, 3};
+
+        for (int entry : intValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        Iterator<Entry<UnsignedInteger, String>> iterator = entries.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            Entry<UnsignedInteger, String> entry = iterator.next();
+            assertNotNull(entry);
+            assertEquals(UnsignedInteger.valueOf(intValues[counter]), entry.getKey());
+            assertEquals("" + intValues[counter++], entry.getValue());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(intValues.length, counter);
+    }
+
+    @Test
+    public void testEntryIterationRemove() {
+        SplayMap<String> map = createMap();
+
+        final int[] intValues = {0, 1, 2, 3};
+
+        for (int entry : intValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        Iterator<Entry<UnsignedInteger, String>> iterator = entries.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            Entry<UnsignedInteger, String> entry = iterator.next();
+            assertNotNull(entry);
+            assertEquals(UnsignedInteger.valueOf(intValues[counter]), entry.getKey());
+            assertEquals("" + intValues[counter++], entry.getValue());
+            iterator.remove();
+        }
+
+        // Check that we really did iterate.
+        assertEquals(intValues.length, counter);
+        assertTrue(map.isEmpty());
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testEntryIterationFollowsUnsignedOrderingExpectations() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        Iterator<Entry<UnsignedInteger, String>> iterator = entries.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        int counter = 0;
+        while (iterator.hasNext()) {
+            Entry<UnsignedInteger, String> entry = iterator.next();
+            assertNotNull(entry);
+            assertEquals(UnsignedInteger.valueOf(expectedOrder[counter]), entry.getKey());
+            assertEquals("" + expectedOrder[counter++], entry.getValue());
+        }
+
+        // Check that we really did iterate.
+        assertEquals(inputValues.length, counter);
+    }
+
+    @Test
+    public void testEntryIterationFailsWhenConcurrentlyModified() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        Iterator<Entry<UnsignedInteger, String>> iterator = entries.iterator();
+        assertNotNull(iterator);
+        assertTrue(iterator.hasNext());
+
+        map.remove(3);
+
+        try {
+            iterator.next();
+            fail("Should not iterate when modified outside of iterator");
+        } catch (ConcurrentModificationException cme) {}
+    }
+
+    @Test
+    public void testEntrySetIterationOnEmptyTree() {
+        SplayMap<String> map = createMap();
+        Set<Entry<UnsignedInteger, String>> entries= map.entrySet();
+        Iterator<Entry<UnsignedInteger, String>> iterator = entries.iterator();
+
+        assertFalse(iterator.hasNext());
+        try {
+            iterator.next();
+            fail("Should have thrown a NoSuchElementException");
+        } catch (NoSuchElementException nse) {
+        }
+    }
+
+    @Test
+    public void testFirstKeyOnEmptyMap() {
+        SplayMap<String> map = new SplayMap<>();
+        assertNull(map.firstKey());
+    }
+
+    @Test
+    public void testFirstKey() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        for (int expected : expectedOrder) {
+            assertEquals(expected, map.firstKey().intValue());
+            map.remove(expected);
+        }
+
+        assertNull(map.firstKey());
+    }
+
+    @Test
+    public void testFirstEntryOnEmptyMap() {
+        SplayMap<String> map = createMap();
+        assertNull(map.firstEntry());
+    }
+
+    @Test
+    public void testFirstEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        for (int expected : expectedOrder) {
+            assertEquals(expected, map.firstEntry().getPrimitiveKey());
+            map.remove(expected);
+        }
+
+        assertNull(map.firstKey());
+    }
+
+    @Test
+    public void testPollFirstEntryEmptyMap() {
+        SplayMap<String> map = createMap();
+        assertNull(map.pollFirstEntry());
+    }
+
+    @Test
+    public void testPollFirstEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        for (int expected : expectedOrder) {
+            assertEquals(expected, map.pollFirstEntry().getPrimitiveKey());
+        }
+
+        assertNull(map.firstKey());
+    }
+
+    @Test
+    public void testLastKeyOnEmptyMap() {
+        SplayMap<String> map = createMap();
+        assertNull(map.lastKey());
+    }
+
+    @Test
+    public void testLastKey() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {-1, -2, 3, 2, 1, 0};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        for (int expected : expectedOrder) {
+            assertEquals(expected, map.lastKey().intValue());
+            map.remove(expected);
+        }
+
+        assertNull(map.lastKey());
+    }
+
+    @Test
+    public void testLastEntryOnEmptyMap() {
+        SplayMap<String> map = createMap();
+        assertNull(map.lastEntry());
+    }
+
+    @Test
+    public void testLastEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {-1, -2, 3, 2, 1, 0};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        for (int expected : expectedOrder) {
+            assertEquals(expected, map.lastEntry().getPrimitiveKey());
+            map.remove(expected);
+        }
+
+        assertNull(map.lastEntry());
+    }
+
+    @Test
+    public void testPollLastEntryEmptyMap() {
+        SplayMap<String> map = createMap();
+        assertNull(map.pollLastEntry());
+    }
+
+    @Test
+    public void testPollLastEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {-1, -2, 3, 2, 1, 0};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        for (int expected : expectedOrder) {
+            assertEquals(expected, map.pollLastEntry().getPrimitiveKey());
+        }
+
+        assertNull(map.lastEntry());
+    }
+
+    @Test
+    public void testForEach() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        final SequenceNumber index = new SequenceNumber(0);
+        map.forEach((k, v) -> {
+            int value = index.getAndIncrement().intValue();
+            assertEquals(expectedOrder[value], k.intValue());
+        });
+
+        assertEquals(index.intValue(), inputValues.length);
+    }
+
+    @Test
+    public void testForEachEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+        final int[] expectedOrder = {0, 1, 2, 3, -2, -1};
+
+        for (int entry : inputValues) {
+            map.put(entry, "" + entry);
+        }
+
+        final SequenceNumber index = new SequenceNumber(0);
+        map.forEach((value) -> {
+            int i = index.getAndIncrement().intValue();
+            assertEquals(expectedOrder[i] + "", value);
+        });
+
+        assertEquals(index.intValue(), inputValues.length);
+    }
+
+    @Test
+    public void testRandomProduceAndConsumeWithBacklog() {
+        SplayMap<String> map = createMap();
+
+        final int ITERATIONS = 8192;
+        final String DUMMY_STRING = "test";
+
+        try {
+            for (int i = 0; i < ITERATIONS; ++i) {
+                map.put(UnsignedInteger.valueOf(i), DUMMY_STRING);
+            }
+
+            for (int i = 0; i < ITERATIONS; ++i) {
+                int p = random.nextInt(ITERATIONS);
+                int c = random.nextInt(ITERATIONS);
+
+                map.put(UnsignedInteger.valueOf(p), DUMMY_STRING);
+                map.remove(UnsignedInteger.valueOf(c));
+            }
+        } catch (Throwable error) {
+            dumpRandomDataSet(ITERATIONS, true);
+            throw error;
+        }
+    }
+
+    @Test
+    public void testRandomPutAndGetIntoEmptyMap() {
+        SplayMap<String> map = createMap();
+
+        final int ITERATIONS = 8192;
+        final String DUMMY_STRING = "test";
+
+        try {
+            for (int i = 0; i < ITERATIONS; ++i) {
+                int p = random.nextInt(ITERATIONS);
+                int c = random.nextInt(ITERATIONS);
+
+                map.put(UnsignedInteger.valueOf(p), DUMMY_STRING);
+                map.remove(UnsignedInteger.valueOf(c));
+            }
+        } catch (AssertionError error) {
+            dumpRandomDataSet(ITERATIONS, true);
+            throw error;
+        }
+    }
+
+    @Test
+    public void testPutRandomValueIntoMapThenRemoveInSameOrder() {
+        SplayMap<String> map = createMap();
+
+        final int ITERATIONS = 8192;
+
+        try {
+            for (int i = 0; i < ITERATIONS; ++i) {
+                final int index = random.nextInt(ITERATIONS);
+                map.put(index, String.valueOf(index));
+            }
+
+            // Reset to verify insertions
+            random.setSeed(seed);
+
+            for (int i = 0; i < ITERATIONS; ++i) {
+                final int index = random.nextInt(ITERATIONS);
+                assertEquals(String.valueOf(index), map.get(index));
+            }
+
+            // Reset to remove
+            random.setSeed(seed);
+
+            for (int i = 0; i < ITERATIONS; ++i) {
+                final int index = random.nextInt(ITERATIONS);
+                map.remove(index);
+            }
+
+            assertTrue(map.isEmpty());
+        } catch (AssertionError error) {
+            dumpRandomDataSet(ITERATIONS, true);
+            throw error;
+        }
+    }
+
+    @Test
+    public void testLowerEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(-2), map.lowerEntry(UnsignedInteger.valueOf(-1)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.lowerEntry(UnsignedInteger.valueOf(-2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.lowerEntry(UnsignedInteger.valueOf(4)).getKey());
+        assertEquals(UnsignedInteger.valueOf(2), map.lowerEntry(UnsignedInteger.valueOf(3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(1), map.lowerEntry(UnsignedInteger.valueOf(2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(0), map.lowerEntry(UnsignedInteger.valueOf(1)).getKey());
+        assertNull(map.lowerEntry(UnsignedInteger.valueOf(0)));
+    }
+
+    @Test
+    public void testLowerKey() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(-2), map.lowerKey(UnsignedInteger.valueOf(-1)));
+        assertEquals(UnsignedInteger.valueOf(3), map.lowerKey(UnsignedInteger.valueOf(-2)));
+        assertEquals(UnsignedInteger.valueOf(3), map.lowerKey(UnsignedInteger.valueOf(4)));
+        assertEquals(UnsignedInteger.valueOf(2), map.lowerKey(UnsignedInteger.valueOf(3)));
+        assertEquals(UnsignedInteger.valueOf(1), map.lowerKey(UnsignedInteger.valueOf(2)));
+        assertEquals(UnsignedInteger.valueOf(0), map.lowerKey(UnsignedInteger.valueOf(1)));
+        assertNull(map.lowerEntry(UnsignedInteger.valueOf(0)));
+    }
+
+    @Test
+    public void testHigherEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(1), map.higherEntry(UnsignedInteger.valueOf(0)).getKey());
+        assertEquals(UnsignedInteger.valueOf(2), map.higherEntry(UnsignedInteger.valueOf(1)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.higherEntry(UnsignedInteger.valueOf(2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.higherEntry(UnsignedInteger.valueOf(3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.higherEntry(UnsignedInteger.valueOf(4)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.higherEntry(UnsignedInteger.valueOf(-3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-1), map.higherEntry(UnsignedInteger.valueOf(-2)).getKey());
+        assertNull(map.higherEntry(UnsignedInteger.valueOf(-1)));
+    }
+
+    @Test
+    public void testHigherKey() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(1), map.higherKey(UnsignedInteger.valueOf(0)));
+        assertEquals(UnsignedInteger.valueOf(2), map.higherKey(UnsignedInteger.valueOf(1)));
+        assertEquals(UnsignedInteger.valueOf(3), map.higherKey(UnsignedInteger.valueOf(2)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.higherKey(UnsignedInteger.valueOf(3)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.higherKey(UnsignedInteger.valueOf(4)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.higherKey(UnsignedInteger.valueOf(-3)));
+        assertEquals(UnsignedInteger.valueOf(-1), map.higherKey(UnsignedInteger.valueOf(-2)));
+        assertNull(map.higherKey(UnsignedInteger.valueOf(-1)));
+    }
+
+    @Test
+    public void testFloorEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(-1), map.floorEntry(UnsignedInteger.valueOf(-1)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.floorEntry(UnsignedInteger.valueOf(-2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.floorEntry(UnsignedInteger.valueOf(4)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.floorEntry(UnsignedInteger.valueOf(-3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.floorEntry(UnsignedInteger.valueOf(Integer.MAX_VALUE)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.floorEntry(UnsignedInteger.valueOf(3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(2), map.floorEntry(UnsignedInteger.valueOf(2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(1), map.floorEntry(UnsignedInteger.valueOf(1)).getKey());
+        assertEquals(UnsignedInteger.valueOf(0), map.floorEntry(UnsignedInteger.valueOf(0)).getKey());
+    }
+
+    @Test
+    public void testFloorKey() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(-1), map.floorKey(UnsignedInteger.valueOf(-1)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.floorKey(UnsignedInteger.valueOf(-2)));
+        assertEquals(UnsignedInteger.valueOf(3), map.floorKey(UnsignedInteger.valueOf(4)));
+        assertEquals(UnsignedInteger.valueOf(3), map.floorKey(UnsignedInteger.valueOf(-3)));
+        assertEquals(UnsignedInteger.valueOf(3), map.floorKey(UnsignedInteger.valueOf(Integer.MAX_VALUE)));
+        assertEquals(UnsignedInteger.valueOf(3), map.floorKey(UnsignedInteger.valueOf(3)));
+        assertEquals(UnsignedInteger.valueOf(2), map.floorKey(UnsignedInteger.valueOf(2)));
+        assertEquals(UnsignedInteger.valueOf(1), map.floorKey(UnsignedInteger.valueOf(1)));
+        assertEquals(UnsignedInteger.valueOf(0), map.floorKey(UnsignedInteger.valueOf(0)));
+    }
+
+    @Test
+    public void testCeilingEntry() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(0), map.ceilingEntry(UnsignedInteger.valueOf(0)).getKey());
+        assertEquals(UnsignedInteger.valueOf(1), map.ceilingEntry(UnsignedInteger.valueOf(1)).getKey());
+        assertEquals(UnsignedInteger.valueOf(2), map.ceilingEntry(UnsignedInteger.valueOf(2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(3), map.ceilingEntry(UnsignedInteger.valueOf(3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingEntry(UnsignedInteger.valueOf(4)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingEntry(UnsignedInteger.valueOf(Integer.MAX_VALUE)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingEntry(UnsignedInteger.valueOf(-3)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingEntry(UnsignedInteger.valueOf(-2)).getKey());
+        assertEquals(UnsignedInteger.valueOf(-1), map.ceilingEntry(UnsignedInteger.valueOf(-1)).getKey());
+    }
+
+    @Test
+    public void testCeilingKey() {
+        SplayMap<String> map = createMap();
+
+        final int[] inputValues = {3, 0, -1, 1, -2, 2};
+
+        for (int entry : inputValues) {
+            map.put(UnsignedInteger.valueOf(entry), "" + entry);
+        }
+
+        assertEquals(UnsignedInteger.valueOf(0), map.ceilingKey(UnsignedInteger.valueOf(0)));
+        assertEquals(UnsignedInteger.valueOf(1), map.ceilingKey(UnsignedInteger.valueOf(1)));
+        assertEquals(UnsignedInteger.valueOf(2), map.ceilingKey(UnsignedInteger.valueOf(2)));
+        assertEquals(UnsignedInteger.valueOf(3), map.ceilingKey(UnsignedInteger.valueOf(3)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingKey(UnsignedInteger.valueOf(4)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingKey(UnsignedInteger.valueOf(Integer.MAX_VALUE)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingKey(UnsignedInteger.valueOf(-3)));
+        assertEquals(UnsignedInteger.valueOf(-2), map.ceilingKey(UnsignedInteger.valueOf(-2)));
+        assertEquals(UnsignedInteger.valueOf(-1), map.ceilingKey(UnsignedInteger.valueOf(-1)));
+    }
+
+    protected void dumpRandomDataSet(int iterations, boolean bounded) {
+        final int[] dataSet = new int[iterations];
+
+        random.setSeed(seed);
+
+        for (int i = 0; i < iterations; ++i) {
+            if (bounded) {
+                dataSet[i] = random.nextInt(iterations);
+            } else {
+                dataSet[i] = random.nextInt();
+            }
+        }
+
+        LOG.info("Random seed was: {}" , seed);
+        LOG.info("Entries in data set: {}", dataSet);
+    }
+
+    protected static class OutsideEntry<K, V> implements Map.Entry<K, V> {
+
+        private final K key;
+        private V value;
+
+        public OutsideEntry(K key, V value) {
+            this.key = key;
+            this.value = value;
+        }
+
+        @Override
+        public V setValue(V value) {
+            V oldValue = this.value;
+            this.value = value;
+            return oldValue;
+        }
+
+        @Override
+        public V getValue() {
+            return value;
+        }
+
+        @Override
+        public K getKey() {
+            return key;
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/BinaryTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/BinaryTest.java
new file mode 100644
index 0000000..e057d4f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/BinaryTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+public class BinaryTest {
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testNotEqualsWithDifferentTypeObject() {
+        Binary binary = createSteppedValueBinary(10);
+
+        assertFalse(binary.equals("not-a-Binary"), "Objects should not be equal with different type");
+    }
+
+    @Test
+    public void testCreateEmptyBinary() {
+        Binary binary = new Binary();
+
+        assertNull(binary.getArray());
+        assertNull(binary.asByteBuffer());
+        assertNull(binary.asProtonBuffer());
+        assertNull(binary.arrayCopy());
+        assertEquals(binary, binary.copy());
+        assertEquals("", binary.toString());
+    }
+
+    @Test
+    public void testEqualsWithItself() {
+        Binary binary = createSteppedValueBinary(10);
+
+        assertTrue(binary.equals(binary), "Object should be equal to itself");
+    }
+
+    @Test
+    public void testEqualsWithDifferentBinaryOfSameLengthAndContent() {
+        int length = 10;
+        Binary bin1 = createSteppedValueBinary(length);
+        Binary bin2 = createSteppedValueBinary(length);
+
+        assertTrue(bin1.equals(bin2), "Objects should be equal");
+        assertTrue(bin2.equals(bin1), "Objects should be equal");
+    }
+
+    @Test
+    public void testEqualsWithDifferentLengthBinaryOfDifferentBytes() {
+        int length1 = 10;
+        Binary bin1 = createSteppedValueBinary(length1);
+        Binary bin2 = createSteppedValueBinary(length1 + 1);
+
+        assertFalse(bin1.equals(bin2), "Objects should not be equal");
+        assertFalse(bin2.equals(bin1), "Objects should not be equal");
+    }
+
+    @Test
+    public void testEqualsWithDifferentLengthBinaryOfSameByte() {
+        Binary bin1 = createNewRepeatedValueBinary(10, (byte) 1);
+        Binary bin2 = createNewRepeatedValueBinary(123, (byte) 1);
+
+        assertFalse(bin1.equals(bin2), "Objects should not be equal");
+        assertFalse(bin2.equals(bin1), "Objects should not be equal");
+    }
+
+    @Test
+    public void testEqualsWithDifferentContentBinary() {
+        int length = 10;
+        Binary bin1 = createNewRepeatedValueBinary(length, (byte) 1);
+
+        Binary bin2 = createNewRepeatedValueBinary(length, (byte) 1);
+        bin2.getArray()[5] = (byte) 0;
+
+        assertFalse(bin1.equals(bin2), "Objects should not be equal");
+        assertFalse(bin2.equals(bin1), "Objects should not be equal");
+    }
+
+    private Binary createSteppedValueBinary(int length) {
+        byte[] bytes = new byte[length];
+        for (int i = 0; i < length; i++) {
+            bytes[i] = (byte) (length - i);
+        }
+
+        return new Binary(bytes);
+    }
+
+    private Binary createNewRepeatedValueBinary(int length, byte repeatedByte) {
+        byte[] bytes = new byte[length];
+        Arrays.fill(bytes, repeatedByte);
+
+        return new Binary(bytes);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/SymbolTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/SymbolTest.java
new file mode 100644
index 0000000..7dd404d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/SymbolTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.junit.jupiter.api.Test;
+
+public class SymbolTest {
+
+    private final String LARGE_SYMBOL_VALUIE = "Large String: " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog. " +
+        "The quick brown fox jumps over the lazy dog.";
+
+
+    @Test
+    public void testGetSymbolWithNullString() {
+        assertNull(Symbol.getSymbol((String) null));
+    }
+
+    @Test
+    public void testGetSymbolWithNullBuffer() {
+        assertNull(Symbol.getSymbol((ProtonBuffer) null));
+    }
+
+    @Test
+    public void testGetSymbolWithEmptyString() {
+        assertNotNull(Symbol.getSymbol(""));
+        assertSame(Symbol.getSymbol(""), Symbol.getSymbol(""));
+    }
+
+    @Test
+    public void testGetSymbolWithEmptyBuffer() {
+        assertNotNull(Symbol.getSymbol(ProtonByteBufferAllocator.DEFAULT.allocate(0)));
+        assertSame(Symbol.getSymbol(ProtonByteBufferAllocator.DEFAULT.allocate(0)),
+                   Symbol.getSymbol(ProtonByteBufferAllocator.DEFAULT.allocate(0)));
+    }
+
+    @Test
+    public void testCompareTo() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+        String symbolString3 = "Symbol-3";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+        Symbol symbol3 = Symbol.valueOf(symbolString3);
+
+        assertEquals(0, symbol1.compareTo(symbol1));
+        assertEquals(0, symbol2.compareTo(symbol2));
+        assertEquals(0, symbol3.compareTo(symbol3));
+
+        assertTrue(symbol2.compareTo(symbol1) > 0);
+        assertTrue(symbol3.compareTo(symbol1) > 0);
+        assertTrue(symbol3.compareTo(symbol2) > 0);
+
+        assertTrue(symbol1.compareTo(symbol2) < 0);
+        assertTrue(symbol1.compareTo(symbol3) < 0);
+        assertTrue(symbol2.compareTo(symbol3) < 0);
+    }
+
+    @Test
+    public void testEquals() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+        String symbolString3 = "Symbol-3";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+        Symbol symbol3 = Symbol.valueOf(symbolString3);
+
+        assertNotEquals(symbol1, symbol2);
+
+        assertEquals(symbolString1, symbol1.toString());
+        assertEquals(symbolString2, symbol2.toString());
+        assertEquals(symbolString3, symbol3.toString());
+
+        assertNotEquals(symbol1, symbol2);
+        assertNotEquals(symbol2, symbol3);
+        assertNotEquals(symbol3, symbol1);
+
+        assertNotEquals(symbolString1, symbol1);
+        assertNotEquals(symbolString2, symbol2);
+        assertNotEquals(symbolString3, symbol3);
+    }
+
+    @Test
+    public void testHashcode() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+
+        assertNotEquals(symbol1, symbol2);
+        assertNotEquals(symbol1.hashCode(), symbol2.hashCode());
+
+        assertEquals(symbol1.hashCode(), Symbol.valueOf(symbolString1).hashCode());
+        assertEquals(symbol2.hashCode(), Symbol.valueOf(symbolString2).hashCode());
+    }
+
+    @Test
+    public void testValueOf() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString1);
+        Symbol symbol2 = Symbol.valueOf(symbolString2);
+
+        assertNotEquals(symbol1, symbol2);
+
+        assertEquals(symbolString1, symbol1.toString());
+        assertEquals(symbolString2, symbol2.toString());
+    }
+
+    @Test
+    public void testValueOfProducesSingleton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString);
+        Symbol symbol2 = Symbol.valueOf(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+    }
+
+    @Test
+    public void testGetSymbol() {
+        String symbolString1 = "Symbol-1";
+        String symbolString2 = "Symbol-2";
+
+        Symbol symbol1 = Symbol.getSymbol(symbolString1);
+        Symbol symbol2 = Symbol.getSymbol(symbolString2);
+
+        assertNotEquals(symbol1, symbol2);
+
+        assertEquals(symbolString1, symbol1.toString());
+        assertEquals(symbolString2, symbol2.toString());
+    }
+
+    @Test
+    public void testGetSymbolProducesSingleton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.getSymbol(symbolString);
+        Symbol symbol2 = Symbol.getSymbol(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+    }
+
+    @Test
+    public void testGetSymbolAndValueOfProduceSingleton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.valueOf(symbolString);
+        Symbol symbol2 = Symbol.getSymbol(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+    }
+
+    @Test
+    public void testToStringProducesSingelton() {
+        String symbolString = "Symbol-String";
+
+        Symbol symbol1 = Symbol.getSymbol(symbolString);
+        Symbol symbol2 = Symbol.getSymbol(symbolString);
+
+        assertEquals(symbolString, symbol1.toString());
+        assertEquals(symbolString, symbol2.toString());
+
+        assertSame(symbol1, symbol2);
+        assertSame(symbol1.toString(), symbol2.toString());
+    }
+
+    @Test
+    public void testLrageSymbolNotCached() {
+        Symbol symbol1 = Symbol.valueOf(LARGE_SYMBOL_VALUIE);
+        Symbol symbol2 = Symbol.getSymbol(
+            ProtonByteBufferAllocator.DEFAULT.wrap(LARGE_SYMBOL_VALUIE.getBytes(StandardCharsets.US_ASCII)));
+
+        assertNotSame(symbol1, symbol2);
+        assertNotSame(symbol1.toString(), symbol2.toString());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedByteTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedByteTest.java
new file mode 100644
index 0000000..b0075b0
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedByteTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedByteTest {
+
+    @Test
+    public void testEquals() {
+        UnsignedByte ubyte1 = UnsignedByte.valueOf((byte) 1);
+        UnsignedByte ubyte2 = UnsignedByte.valueOf((byte) 2);
+
+        assertEquals(ubyte1, ubyte1);
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1, ubyte2);
+
+        assertNotEquals(ubyte1, "test");
+        assertFalse(ubyte1.equals(null));
+
+        assertEquals(ubyte1, UnsignedByte.valueOf((byte) 1));
+        assertEquals(ubyte2, UnsignedByte.valueOf((byte) 2));
+
+        assertSame(ubyte1, UnsignedByte.valueOf((byte) 1));
+        assertSame(ubyte2, UnsignedByte.valueOf((byte) 2));
+
+        UnsignedByte ubyte3 = UnsignedByte.valueOf((byte) 255);
+        UnsignedByte ubyte4 = UnsignedByte.valueOf((byte) 255);
+
+        assertNotSame(ubyte3, new UnsignedByte((byte) 255));
+        assertNotSame(ubyte4, new UnsignedByte((byte) 255));
+
+        assertEquals(ubyte3, new UnsignedByte((byte) 255));
+        assertEquals(ubyte4, new UnsignedByte((byte) 255));
+    }
+
+    @Test
+    public void testToString() {
+        assertEquals("0", UnsignedByte.valueOf((byte) 0).toString());
+        assertEquals("255", UnsignedByte.valueOf((byte) 255).toString());
+        assertEquals("127", UnsignedByte.valueOf((byte) 127).toString());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedByte ubyte1 = UnsignedByte.valueOf((byte) 1);
+        UnsignedByte ubyte2 = UnsignedByte.valueOf((byte) 2);
+
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1.hashCode(), ubyte2.hashCode());
+
+        assertEquals(ubyte1.hashCode(), UnsignedByte.valueOf((byte) 1).hashCode());
+        assertEquals(ubyte2.hashCode(), UnsignedByte.valueOf((byte) 2).hashCode());
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedByte.valueOf((byte) 0).shortValue());
+        assertEquals((short) 255, UnsignedByte.valueOf((byte) 255).shortValue());
+        assertEquals((short) 1, UnsignedByte.valueOf((byte) 1).shortValue());
+        assertEquals((short) 127, UnsignedByte.valueOf((byte) 127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedByte.valueOf((byte) 0).intValue());
+        assertEquals(255, UnsignedByte.valueOf((byte) 255).intValue());
+        assertEquals(1, UnsignedByte.valueOf((byte) 1).intValue());
+        assertEquals(127, UnsignedByte.valueOf((byte) 127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        assertEquals(0l, UnsignedByte.valueOf((byte) 0).longValue());
+        assertEquals(255l, UnsignedByte.valueOf((byte) 255).longValue());
+        assertEquals(1l, UnsignedByte.valueOf((byte) 1).longValue());
+        assertEquals(127l, UnsignedByte.valueOf((byte) 127).longValue());
+    }
+
+    @Test
+    public void testFloatValue() {
+        assertEquals(0, UnsignedByte.valueOf((byte) 0).floatValue());
+        assertEquals(255, UnsignedByte.valueOf((byte) 255).floatValue());
+        assertEquals(1, UnsignedByte.valueOf((byte) 1).floatValue());
+        assertEquals(127, UnsignedByte.valueOf((byte) 127).floatValue());
+    }
+
+    @Test
+    public void testDoubleValue() {
+        assertEquals(0, UnsignedByte.valueOf((byte) 0).doubleValue());
+        assertEquals(255, UnsignedByte.valueOf((byte) 255).doubleValue());
+        assertEquals(1, UnsignedByte.valueOf((byte) 1).doubleValue());
+        assertEquals(127, UnsignedByte.valueOf((byte) 127).doubleValue());
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo((byte) 255) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo((byte) 0) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 127).compareTo((byte) 126) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 32).compareTo((byte) 64) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo((byte) 127) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 126).compareTo((byte) 255) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo((byte) 0) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo((byte) 255) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedByte() {
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo(UnsignedByte.valueOf((byte) 255)) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo(UnsignedByte.valueOf((byte) 0)) == 0);
+        assertTrue(UnsignedByte.valueOf((byte) 127).compareTo(UnsignedByte.valueOf((byte) 126)) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 32).compareTo(UnsignedByte.valueOf((byte) 64)) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo(UnsignedByte.valueOf((byte) 127)) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 126).compareTo(UnsignedByte.valueOf((byte) 255)) < 0);
+        assertTrue(UnsignedByte.valueOf((byte) 255).compareTo(UnsignedByte.valueOf((byte) 0)) > 0);
+        assertTrue(UnsignedByte.valueOf((byte) 0).compareTo(UnsignedByte.valueOf((byte) 255)) < 0);
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedByte.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithTextThrowsNFE() throws Exception {
+        try {
+            UnsignedByte.valueOf("TEST");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithOutOfRangeValueThrowsNFE() throws Exception {
+        assertEquals(255, UnsignedByte.valueOf("255").intValue());
+
+        try {
+            UnsignedByte.valueOf("256");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testToUnsignedShortValueFromByte() {
+        short shortValue = Byte.MAX_VALUE + 1;
+
+        assertEquals(0, UnsignedByte.toUnsignedShort((byte) 0));
+        assertEquals(255, UnsignedByte.toUnsignedShort((byte) 255));
+        assertEquals(1, UnsignedByte.toUnsignedShort((byte) 1));
+        assertEquals(127, UnsignedByte.toUnsignedShort((byte) 127));
+        assertEquals(shortValue, UnsignedByte.toUnsignedShort((byte) (Byte.MAX_VALUE + 1)));
+    }
+
+    @Test
+    public void testToUnsignedIntValueFromByte() {
+        int intValue = Byte.MAX_VALUE + 1;
+
+        assertEquals(0, UnsignedByte.toUnsignedInt((byte) 0));
+        assertEquals(255, UnsignedByte.toUnsignedInt((byte) 255));
+        assertEquals(1, UnsignedByte.toUnsignedInt((byte) 1));
+        assertEquals(127, UnsignedByte.toUnsignedInt((byte) 127));
+        assertEquals(intValue, UnsignedByte.toUnsignedInt((byte) (Byte.MAX_VALUE + 1)));
+    }
+
+    @Test
+    public void testToUnsignedLongValueFromByte() {
+        long longValue = (long) Byte.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedByte.toUnsignedLong((byte) 0));
+        assertEquals(255l, UnsignedByte.toUnsignedLong((byte) 255));
+        assertEquals(1l, UnsignedByte.toUnsignedLong((byte) 1));
+        assertEquals(127l, UnsignedByte.toUnsignedLong((byte) 127));
+        assertEquals(longValue, UnsignedByte.toUnsignedLong((byte) (Byte.MAX_VALUE + 1)));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedIntegerTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedIntegerTest.java
new file mode 100644
index 0000000..2be3070
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedIntegerTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedIntegerTest {
+
+    @Test
+    public void testToString() {
+        assertEquals("0",  UnsignedInteger.valueOf(0).toString());
+        assertEquals("65535", UnsignedInteger.valueOf(65535).toString());
+        assertEquals("127", UnsignedInteger.valueOf(127).toString());
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testEquals() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(2);
+
+        assertEquals(uint1, uint1);
+        assertEquals(uint1, UnsignedInteger.valueOf(1));
+        assertEquals(uint1, UnsignedInteger.valueOf("1"));
+        assertFalse(uint1.equals(uint2));
+        assertNotEquals(uint1.hashCode(), uint2.hashCode());
+
+        assertEquals(uint1.hashCode(), UnsignedInteger.valueOf(1).hashCode());
+        assertEquals(uint2.hashCode(), UnsignedInteger.valueOf(2).hashCode());
+
+        assertFalse(uint1.equals(null));
+        assertFalse(uint1.equals("test"));
+    }
+
+    @Test
+    public void testValueOfFromLong() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1l);
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(longValue);
+
+        assertEquals(1, uint1.intValue());
+        assertEquals(longValue, uint2.longValue());
+    }
+
+    @Test
+    public void testValueOfFromString() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        UnsignedInteger uint1 = UnsignedInteger.valueOf("1");
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(String.valueOf(longValue));
+
+        assertEquals(1, uint1.intValue());
+        assertEquals(longValue, uint2.longValue());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger uint2 = UnsignedInteger.valueOf(2);
+
+        assertNotEquals(uint1, uint2);
+        assertNotEquals(uint1.hashCode(), uint2.hashCode());
+
+        assertEquals(uint1.hashCode(), UnsignedInteger.valueOf(1).hashCode());
+        assertEquals(uint2.hashCode(), UnsignedInteger.valueOf(2).hashCode());
+    }
+
+    @Test
+    public void testAdd() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger result = uint1.add(uint1);
+        assertEquals(2, result.intValue());
+    }
+
+    @Test
+    public void testSubtract() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        UnsignedInteger result = uint1.subtract(uint1);
+        assertEquals(0, result.intValue());
+    }
+
+    @Test
+    public void testCompareToPrimitiveInt() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        assertEquals(0, uint1.compareTo(1));
+        assertEquals(1, uint1.compareTo(0));
+        assertEquals(-1, uint1.compareTo(2));
+    }
+
+    @Test
+    public void testCompareToPrimitiveLong() {
+        UnsignedInteger uint1 = UnsignedInteger.valueOf(1);
+        assertEquals(0, uint1.compareTo(1l));
+        assertEquals(1, uint1.compareTo(0l));
+        assertEquals(-1, uint1.compareTo(2l));
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedInteger.valueOf(0).shortValue());
+        assertEquals((short) 65535, UnsignedInteger.valueOf(65535).shortValue());
+        assertEquals((short) 1, UnsignedInteger.valueOf(1).shortValue());
+        assertEquals((short) 127, UnsignedInteger.valueOf(127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedInteger.valueOf(0).intValue());
+        assertEquals(65535, UnsignedInteger.valueOf(65535).intValue());
+        assertEquals(1, UnsignedInteger.valueOf(1).intValue());
+        assertEquals(127, UnsignedInteger.valueOf(127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedInteger.valueOf(0).longValue());
+        assertEquals(65535l, UnsignedInteger.valueOf(65535).longValue());
+        assertEquals(1l, UnsignedInteger.valueOf(1).longValue());
+        assertEquals(127l, UnsignedInteger.valueOf(127).longValue());
+        assertEquals(longValue, UnsignedInteger.valueOf(Integer.MAX_VALUE + 1).longValue());
+    }
+
+    @Test
+    public void testToUnsignedLongValueFromInt() {
+        long longValue = (long) Integer.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedInteger.toUnsignedLong(0));
+        assertEquals(65535l, UnsignedInteger.toUnsignedLong(65535));
+        assertEquals(1l, UnsignedInteger.toUnsignedLong(1));
+        assertEquals(127l, UnsignedInteger.toUnsignedLong(127));
+        assertEquals(longValue, UnsignedInteger.toUnsignedLong(Integer.MAX_VALUE + 1));
+    }
+
+    @Test
+    public void testFloatValue() {
+        assertEquals(0.0f, UnsignedInteger.valueOf(0).floatValue(), 0.0f);
+        assertEquals(65535.0f, UnsignedInteger.valueOf(65535).floatValue(), 0.0f);
+        assertEquals(1.0f, UnsignedInteger.valueOf(1).floatValue(), 0.0f);
+        assertEquals(127.0f, UnsignedInteger.valueOf(127).floatValue(), 0.0f);
+    }
+
+    @Test
+    public void testDoubleValue() {
+        assertEquals(0.0, UnsignedInteger.valueOf(0).doubleValue(), 0.0);
+        assertEquals(65535.0, UnsignedInteger.valueOf(65535).doubleValue(), 0.0);
+        assertEquals(1.0, UnsignedInteger.valueOf(1).doubleValue(), 0.0);
+        assertEquals(127.0, UnsignedInteger.valueOf(127).doubleValue(), 0.0);
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedInteger.valueOf(255).compareTo(255) == 0);
+        assertTrue(UnsignedInteger.valueOf(0).compareTo(0) == 0);
+        assertTrue(UnsignedInteger.valueOf(127).compareTo(126) > 0);
+        assertTrue(UnsignedInteger.valueOf(32).compareTo(64) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedInteger() {
+        assertTrue(UnsignedInteger.valueOf(65535).compareTo(UnsignedInteger.valueOf(65535)) == 0);
+        assertTrue(UnsignedInteger.valueOf(0).compareTo(UnsignedInteger.valueOf(0)) == 0);
+        assertTrue(UnsignedInteger.valueOf(127).compareTo(UnsignedInteger.valueOf(126)) > 0);
+        assertTrue(UnsignedInteger.valueOf(32).compareTo(UnsignedInteger.valueOf(64)) < 0);
+    }
+
+    @Test
+    public void testCompareToIntInt() {
+        assertTrue(UnsignedInteger.compare(65536, 65536) == 0);
+        assertTrue(UnsignedInteger.compare(0, 0) == 0);
+        assertTrue(UnsignedInteger.compare(1, 2) < 0);
+        assertTrue(UnsignedInteger.compare(127, 32) > 0);
+    }
+
+    @Test
+    public void testCompareToLongLong() {
+        assertTrue(UnsignedInteger.compare(65536l, 65536l) == 0);
+        assertTrue(UnsignedInteger.compare(0l, 0l) == 0);
+        assertTrue(UnsignedInteger.compare(1l, 2l) < 0);
+        assertTrue(UnsignedInteger.compare(127l, 32l) > 0);
+    }
+
+    @Test
+    public void testValueOfLongWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf(-1l);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfLongWithLargeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf(Long.MAX_VALUE);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithTextThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf("TEST");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithOutOfRangeValueThrowsNFE() throws Exception {
+        try {
+            UnsignedInteger.valueOf("" + Long.MAX_VALUE);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedLongTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedLongTest.java
new file mode 100644
index 0000000..d39c4c0
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedLongTest.java
@@ -0,0 +1,241 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.math.BigInteger;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedLongTest {
+
+    private static final byte[] TWO_TO_64_PLUS_ONE_BYTES =
+        new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 1 };
+    private static final byte[] TWO_TO_64_MINUS_ONE_BYTES =
+        new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 };
+
+    @Test
+    public void testToString() {
+        assertEquals("0",  UnsignedLong.valueOf(0).toString());
+        assertEquals("65535", UnsignedLong.valueOf(65535).toString());
+        assertEquals("127", UnsignedLong.valueOf(127).toString());
+    }
+
+    @Test
+    public void testEquals() {
+        UnsignedLong ubyte1 = UnsignedLong.valueOf(1);
+        UnsignedLong ubyte2 = UnsignedLong.valueOf(2);
+
+        assertEquals(ubyte1, ubyte1);
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1, ubyte2);
+
+        assertNotEquals(ubyte1, "test");
+        assertFalse(ubyte1.equals(null));
+
+        assertEquals(ubyte1, UnsignedLong.valueOf(1));
+        assertEquals(ubyte2, UnsignedLong.valueOf(2));
+
+        assertSame(ubyte1, UnsignedLong.valueOf(1));
+        assertSame(ubyte2, UnsignedLong.valueOf(2));
+
+        UnsignedLong ubyte3 = UnsignedLong.valueOf(32767);
+        UnsignedLong ubyte4 = UnsignedLong.valueOf(32767);
+
+        assertNotSame(ubyte3, UnsignedLong.valueOf(32767));
+        assertNotSame(ubyte4, UnsignedLong.valueOf(32767));
+
+        assertEquals(ubyte3, UnsignedLong.valueOf(32767));
+        assertEquals(ubyte4, UnsignedLong.valueOf(32767));
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedLong ubyte1 = UnsignedLong.valueOf((short) 1);
+        UnsignedLong ubyte2 = UnsignedLong.valueOf((short) 2);
+
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1.hashCode(), ubyte2.hashCode());
+
+        assertEquals(ubyte1.hashCode(), UnsignedLong.valueOf((short) 1).hashCode());
+        assertEquals(ubyte2.hashCode(), UnsignedLong.valueOf((short) 2).hashCode());
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedLong.valueOf(0).shortValue());
+        assertEquals((short) 65535, UnsignedLong.valueOf(65535).shortValue());
+        assertEquals((short) 1, UnsignedLong.valueOf(1).shortValue());
+        assertEquals((short) 127, UnsignedLong.valueOf(127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedLong.valueOf(0).intValue());
+        assertEquals(65535, UnsignedLong.valueOf(65535).intValue());
+        assertEquals(1, UnsignedLong.valueOf(1).intValue());
+        assertEquals(127, UnsignedLong.valueOf(127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        assertEquals(0l, UnsignedLong.valueOf(0).longValue());
+        assertEquals(65535l, UnsignedLong.valueOf(65535).longValue());
+        assertEquals(1l, UnsignedLong.valueOf(1).longValue());
+        assertEquals(127l, UnsignedLong.valueOf(127).longValue());
+    }
+
+    @Test
+    public void testFloatValue() {
+        assertEquals(0, UnsignedLong.valueOf(0).floatValue());
+        assertEquals(65535, UnsignedLong.valueOf(65535).floatValue());
+        assertEquals(1, UnsignedLong.valueOf(1).floatValue());
+        assertEquals(127, UnsignedLong.valueOf(127).floatValue());
+    }
+
+    @Test
+    public void testDoubleValue() {
+        assertEquals(0, UnsignedLong.valueOf(0).doubleValue());
+        assertEquals(65535, UnsignedLong.valueOf(65535).doubleValue());
+        assertEquals(1, UnsignedLong.valueOf(1).doubleValue());
+        assertEquals(127, UnsignedLong.valueOf(127).doubleValue());
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedLong.valueOf(255).compareTo(255) == 0);
+        assertTrue(UnsignedLong.valueOf(0).compareTo(0) == 0);
+        assertTrue(UnsignedLong.valueOf(127).compareTo(126) > 0);
+        assertTrue(UnsignedLong.valueOf(32).compareTo(64) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedInteger() {
+        assertTrue(UnsignedLong.valueOf(65535).compareTo(UnsignedLong.valueOf(65535)) == 0);
+        assertTrue(UnsignedLong.valueOf(0).compareTo(UnsignedLong.valueOf(0)) == 0);
+        assertTrue(UnsignedLong.valueOf(127).compareTo(UnsignedLong.valueOf(126)) > 0);
+        assertTrue(UnsignedLong.valueOf(32).compareTo(UnsignedLong.valueOf(64)) < 0);
+    }
+
+    @Test
+    public void testCompareLongs() {
+        assertTrue(UnsignedLong.compare(65535, 65535) == 0);
+        assertTrue(UnsignedLong.compare(0, 0) == 0);
+        assertTrue(UnsignedLong.compare(127, 126) > 0);
+        assertTrue(UnsignedLong.compare(32, 64) < 0);
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        assertEquals(Long.MAX_VALUE, UnsignedLong.valueOf("9223372036854775807").longValue());
+
+        try {
+            UnsignedLong.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfBigIntegerWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedLong.valueOf(BigInteger.valueOf(-1L));
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValuesOfStringWithinRangeSucceed() throws Exception {
+        // check 0 (min) to confirm success
+        UnsignedLong min = UnsignedLong.valueOf("0");
+        assertEquals(0, min.longValue(), "unexpected value");
+
+        // check 2^64 -1 (max) to confirm success
+        BigInteger onLimit = new BigInteger(TWO_TO_64_MINUS_ONE_BYTES);
+        String onlimitString = onLimit.toString();
+        UnsignedLong max = UnsignedLong.valueOf(onlimitString);
+        assertEquals(onLimit, max.bigIntegerValue(), "unexpected value");
+    }
+
+    @Test
+    public void testValuesOfBigIntegerWithinRangeSucceed() throws Exception {
+        // check 0 (min) to confirm success
+        UnsignedLong min = UnsignedLong.valueOf(BigInteger.ZERO);
+        assertEquals(0, min.longValue(), "unexpected value");
+
+        // check 2^64 -1 (max) to confirm success
+        BigInteger onLimit = new BigInteger(TWO_TO_64_MINUS_ONE_BYTES);
+        UnsignedLong max = UnsignedLong.valueOf(onLimit);
+        assertEquals(onLimit, max.bigIntegerValue(), "unexpected value");
+
+        // check Long.MAX_VALUE to confirm success
+        UnsignedLong longMax = UnsignedLong.valueOf(BigInteger.valueOf(Long.MAX_VALUE));
+        assertEquals(Long.MAX_VALUE, longMax.longValue(), "unexpected value");
+    }
+
+    @Test
+    public void testValueOfStringAboveMaxValueThrowsNFE() throws Exception {
+        // 2^64 + 1 (value 2 over max)
+        BigInteger aboveLimit = new BigInteger(TWO_TO_64_PLUS_ONE_BYTES);
+        try {
+            UnsignedLong.valueOf(aboveLimit.toString());
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+
+        // 2^64 (value 1 over max)
+        aboveLimit = aboveLimit.subtract(BigInteger.ONE);
+        try {
+            UnsignedLong.valueOf(aboveLimit.toString());
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfBigIntegerAboveMaxValueThrowsNFE() throws Exception {
+        // 2^64 + 1 (value 2 over max)
+        BigInteger aboveLimit = new BigInteger(TWO_TO_64_PLUS_ONE_BYTES);
+        try {
+            UnsignedLong.valueOf(aboveLimit);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+
+        // 2^64 (value 1 over max)
+        aboveLimit = aboveLimit.subtract(BigInteger.ONE);
+        try {
+            UnsignedLong.valueOf(aboveLimit);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedShortTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedShortTest.java
new file mode 100644
index 0000000..13598b5
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/UnsignedShortTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+public class UnsignedShortTest {
+
+    @Test
+    public void testToString() {
+        assertEquals("0",  UnsignedShort.valueOf((short) 0).toString());
+        assertEquals("65535", UnsignedShort.valueOf((short) 65535).toString());
+        assertEquals("127", UnsignedShort.valueOf((short) 127).toString());
+    }
+
+    @Test
+    public void testHashcode() {
+        UnsignedShort ubyte1 = UnsignedShort.valueOf((short) 1);
+        UnsignedShort ubyte2 = UnsignedShort.valueOf((short) 2);
+
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1.hashCode(), ubyte2.hashCode());
+
+        assertEquals(ubyte1.hashCode(), UnsignedShort.valueOf((short) 1).hashCode());
+        assertEquals(ubyte2.hashCode(), UnsignedShort.valueOf((short) 2).hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        UnsignedShort ubyte1 = UnsignedShort.valueOf((short) 1);
+        UnsignedShort ubyte2 = UnsignedShort.valueOf((short) 2);
+
+        assertEquals(ubyte1, ubyte1);
+        assertNotEquals(ubyte1, ubyte2);
+        assertNotEquals(ubyte1, ubyte2);
+
+        assertNotEquals(ubyte1, "test");
+        assertFalse(ubyte1.equals(null));
+
+        assertEquals(ubyte1, UnsignedShort.valueOf((short) 1));
+        assertEquals(ubyte2, UnsignedShort.valueOf((short) 2));
+
+        assertSame(ubyte1, UnsignedShort.valueOf((short) 1));
+        assertSame(ubyte2, UnsignedShort.valueOf((short) 2));
+
+        UnsignedShort ubyte3 = UnsignedShort.valueOf((short) 32767);
+        UnsignedShort ubyte4 = UnsignedShort.valueOf((short) 32767);
+
+        assertNotSame(ubyte3, UnsignedShort.valueOf((short) 32767));
+        assertNotSame(ubyte4, UnsignedShort.valueOf((short) 32767));
+
+        assertEquals(ubyte3, UnsignedShort.valueOf((short) 32767));
+        assertEquals(ubyte4, UnsignedShort.valueOf((short) 32767));
+    }
+
+    @Test
+    public void testShortValue() {
+        assertEquals((short) 0, UnsignedShort.valueOf((short) 0).shortValue());
+        assertEquals((short) 65535, UnsignedShort.valueOf((short) 65535).shortValue());
+        assertEquals((short) 1, UnsignedShort.valueOf((short) 1).shortValue());
+        assertEquals((short) 127, UnsignedShort.valueOf((short) 127).shortValue());
+    }
+
+    @Test
+    public void testIntValue() {
+        assertEquals(0, UnsignedShort.valueOf((short) 0).intValue());
+        assertEquals(65535, UnsignedShort.valueOf((short) 65535).intValue());
+        assertEquals(1, UnsignedShort.valueOf((short) 1).intValue());
+        assertEquals(127, UnsignedShort.valueOf((short) 127).intValue());
+    }
+
+    @Test
+    public void testLongValue() {
+        assertEquals(0l, UnsignedShort.valueOf((short) 0).longValue());
+        assertEquals(65535l, UnsignedShort.valueOf((short) 65535).longValue());
+        assertEquals(1l, UnsignedShort.valueOf((short) 1).longValue());
+        assertEquals(127l, UnsignedShort.valueOf((short) 127).longValue());
+    }
+
+    @Test
+    public void testFloatValue() {
+        assertEquals(0, UnsignedShort.valueOf((short) 0).floatValue());
+        assertEquals(65535, UnsignedShort.valueOf((short) 65535).floatValue());
+        assertEquals(1, UnsignedShort.valueOf((short) 1).floatValue());
+        assertEquals(127, UnsignedShort.valueOf((short) 127).floatValue());
+    }
+
+    @Test
+    public void testDoubleValue() {
+        assertEquals(0, UnsignedShort.valueOf((short) 0).doubleValue());
+        assertEquals(65535, UnsignedShort.valueOf((short) 65535).doubleValue());
+        assertEquals(1, UnsignedShort.valueOf((short) 1).doubleValue());
+        assertEquals(127, UnsignedShort.valueOf((short) 127).doubleValue());
+    }
+
+    @Test
+    public void testCompareToByte() {
+        assertTrue(UnsignedShort.valueOf((short) 255).compareTo((short) 255) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 0).compareTo((short) 0) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 127).compareTo((short) 126) > 0);
+        assertTrue(UnsignedShort.valueOf((short) 32).compareTo((short) 64) < 0);
+    }
+
+    @Test
+    public void testCompareToUnsignedShort() {
+        assertTrue(UnsignedShort.valueOf((short) 65535).compareTo(UnsignedShort.valueOf((short) 65535)) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 0).compareTo(UnsignedShort.valueOf((short) 0)) == 0);
+        assertTrue(UnsignedShort.valueOf((short) 127).compareTo(UnsignedShort.valueOf((short) 126)) > 0);
+        assertTrue(UnsignedShort.valueOf((short) 32).compareTo(UnsignedShort.valueOf((short) 64)) < 0);
+    }
+
+    @Test
+    public void testCompareToIntInt() {
+        assertTrue(UnsignedShort.compare((short) 65536, (short) 65536) == 0);
+        assertTrue(UnsignedShort.compare((short) 0, (short) 0) == 0);
+        assertTrue(UnsignedShort.compare((short) 1, (short) 2) < 0);
+        assertTrue(UnsignedShort.compare((short) 127, (short) 32) > 0);
+    }
+
+    @Test
+    public void testValueOfIntWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf(-1);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfIntWithLargeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf(Integer.MAX_VALUE);
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithNegativeNumberThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf("-1");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithTextThrowsNFE() throws Exception {
+        try {
+            UnsignedShort.valueOf("TEST");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValueOfStringWithOutOfRangeValueThrowsNFE() throws Exception {
+        assertEquals(65535, UnsignedShort.valueOf("65535").intValue());
+
+        try {
+            UnsignedShort.valueOf("65536");
+            fail("Expected exception was not thrown");
+        } catch (NumberFormatException nfe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testToUnsignedIntValueFromShort() {
+        long longValue = (long) Short.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedShort.toUnsignedInt((short) 0));
+        assertEquals(65535l, UnsignedShort.toUnsignedInt((short) 65535));
+        assertEquals(1l, UnsignedShort.toUnsignedInt((short) 1));
+        assertEquals(127l, UnsignedShort.toUnsignedInt((short) 127));
+        assertEquals(longValue, UnsignedShort.toUnsignedInt((short) (Short.MAX_VALUE + 1)));
+    }
+
+    @Test
+    public void testToUnsignedLongValueFromShort() {
+        long longValue = (long) Short.MAX_VALUE + 1;
+
+        assertEquals(0l, UnsignedShort.toUnsignedLong((short) 0));
+        assertEquals(65535l, UnsignedShort.toUnsignedLong((short) 65535));
+        assertEquals(1l, UnsignedShort.toUnsignedLong((short) 1));
+        assertEquals(127l, UnsignedShort.toUnsignedLong((short) 127));
+        assertEquals(longValue, UnsignedShort.toUnsignedLong((short) (Short.MAX_VALUE + 1)));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AcceptedTypeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AcceptedTypeTest.java
new file mode 100644
index 0000000..e97b463
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AcceptedTypeTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.qpid.protonj2.types.transport.DeliveryState;
+import org.junit.jupiter.api.Test;
+
+public class AcceptedTypeTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(Accepted.getInstance().toString());
+    }
+
+    @Test
+    public void testSingleton() {
+        assertSame(Accepted.getInstance(), Accepted.getInstance());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryState.DeliveryStateType.Accepted, Accepted.getInstance().getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AmqpSequenceTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AmqpSequenceTest.java
new file mode 100644
index 0000000..d82f1c3
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AmqpSequenceTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class AmqpSequenceTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new AmqpSequence<>(null).toString());
+    }
+
+    @Test
+    public void testGetSequenceFromEmptySection() {
+        assertNull(new AmqpSequence<>(null).getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new AmqpSequence<>(null).copy().getValue());
+    }
+
+    @Test
+    public void testCopy() {
+        ArrayList<Object> payload = new ArrayList<>();
+        payload.add("test");
+
+        AmqpSequence<Object> original = new AmqpSequence<>(payload);
+        AmqpSequence<Object> copy = original.copy();
+
+        assertNotSame(original, copy);
+        assertNotSame(original.getValue(), copy.getValue());
+        assertEquals(original.getValue(), copy.getValue());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.AmqpSequence, new AmqpSequence<>(null).getType());
+    }
+
+    @Test
+    public void testHashCode() {
+        List<String> first = new ArrayList<>();
+        first.add("first");
+
+        List<String> second = new ArrayList<>();
+        second.add("second");
+
+        AmqpSequence<String> original = new AmqpSequence<>(first);
+        AmqpSequence<String> copy = original.copy();
+        AmqpSequence<String> another = new AmqpSequence<>(second);
+
+        assertEquals(original.hashCode(), copy.hashCode());
+        assertNotEquals(original.hashCode(), another.hashCode());
+
+        AmqpSequence<String> empty = new AmqpSequence<>(null);
+        AmqpSequence<String> empty2 = new AmqpSequence<>(null);
+
+        assertEquals(empty2.hashCode(), empty.hashCode());
+        assertNotEquals(original.hashCode(), empty.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        List<String> first = new ArrayList<>();
+        first.add("first");
+
+        List<String> second = new ArrayList<>();
+        second.add("second");
+
+        AmqpSequence<String> original = new AmqpSequence<>(first);
+        AmqpSequence<String> copy = original.copy();
+        AmqpSequence<String> another = new AmqpSequence<>(second);
+        AmqpSequence<String> empty = new AmqpSequence<>(null);
+        AmqpSequence<String> empty2 = new AmqpSequence<>(null);
+
+        assertEquals(original, original);
+        assertEquals(original, copy);
+        assertNotEquals(original, another);
+        assertNotEquals(original, "test");
+        assertNotEquals(original, empty);
+        assertNotEquals(empty, original);
+        assertEquals(empty, empty2);
+
+        assertFalse(original.equals(null));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AmqpValueTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AmqpValueTest.java
new file mode 100644
index 0000000..99bfffc
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/AmqpValueTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class AmqpValueTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new AmqpValue<>(null).toString());
+    }
+
+    @Test
+    public void testGetValueFromEmptySection() {
+        assertNull(new AmqpValue<>(null).getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new AmqpValue<>(null).copy().getValue());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.AmqpValue, new AmqpValue<>(null).getType());
+    }
+
+    @Test
+    public void testHashCode() {
+        String first = new String("first");
+        String second = new String("second");
+
+        AmqpValue<String> original = new AmqpValue<>(first);
+        AmqpValue<String> copy = original.copy();
+        AmqpValue<String> another = new AmqpValue<>(second);
+
+        assertEquals(original.hashCode(), copy.hashCode());
+        assertNotEquals(original.hashCode(), another.hashCode());
+
+        AmqpValue<String> empty = new AmqpValue<>(null);
+        AmqpValue<String> empty2 = new AmqpValue<>(null);
+
+        assertEquals(empty2.hashCode(), empty.hashCode());
+        assertNotEquals(original.hashCode(), empty.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        String first = new String("first");
+        String second = new String("second");
+
+        AmqpValue<String> original = new AmqpValue<>(first);
+        AmqpValue<String> copy = original.copy();
+        AmqpValue<String> another = new AmqpValue<>(second);
+        AmqpValue<String> empty = new AmqpValue<>(null);
+        AmqpValue<String> empty2 = new AmqpValue<>(null);
+
+        assertEquals(original, original);
+        assertEquals(original, copy);
+        assertNotEquals(original, another);
+        assertNotEquals(original, "test");
+        assertNotEquals(original, empty);
+        assertNotEquals(empty, original);
+        assertEquals(empty, empty2);
+
+        assertFalse(original.equals(null));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ApplicationPropertiesTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ApplicationPropertiesTest.java
new file mode 100644
index 0000000..6c02271
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ApplicationPropertiesTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class ApplicationPropertiesTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new ApplicationProperties(null).toString());
+    }
+
+    @Test
+    public void testGetPropertiesFromEmptySection() {
+        assertNull(new ApplicationProperties(null).getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new ApplicationProperties(null).copy().getValue());
+    }
+
+    @Test
+    public void testCopy() {
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("key", "value");
+
+        ApplicationProperties original = new ApplicationProperties(payload);
+        ApplicationProperties copy = original.copy();
+
+        assertNotSame(original, copy);
+        assertNotSame(original.getValue(), copy.getValue());
+        assertEquals(original.getValue(), copy.getValue());
+    }
+
+    @Test
+    public void testHashCode() {
+        Map<String, Object> payload1 = new HashMap<>();
+        payload1.put("key", "value");
+
+        Map<String, Object> payload2 = new HashMap<>();
+        payload2.put("key1", "value");
+        payload2.put("key2", "value");
+
+        ApplicationProperties original = new ApplicationProperties(payload1);
+        ApplicationProperties copy = original.copy();
+        ApplicationProperties another = new ApplicationProperties(payload2);
+
+        assertEquals(original.hashCode(), copy.hashCode());
+        assertNotEquals(original.hashCode(), another.hashCode());
+
+        ApplicationProperties empty = new ApplicationProperties(null);
+        ApplicationProperties empty2 = new ApplicationProperties(null);
+
+        assertEquals(empty2.hashCode(), empty.hashCode());
+        assertNotEquals(original.hashCode(), empty.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        Map<String, Object> payload1 = new HashMap<>();
+        payload1.put("key", "value");
+
+        Map<String, Object> payload2 = new HashMap<>();
+        payload2.put("key1", "value");
+        payload2.put("key2", "value");
+
+        ApplicationProperties original = new ApplicationProperties(payload1);
+        ApplicationProperties copy = original.copy();
+        ApplicationProperties another = new ApplicationProperties(payload2);
+        ApplicationProperties empty = new ApplicationProperties(null);
+        ApplicationProperties empty2 = new ApplicationProperties(null);
+
+        assertEquals(original, original);
+        assertEquals(original, copy);
+        assertNotEquals(original, another);
+        assertNotEquals(original, "test");
+        assertNotEquals(original, empty);
+        assertNotEquals(empty, original);
+        assertEquals(empty, empty2);
+
+        assertFalse(original.equals(null));
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.ApplicationProperties, new ApplicationProperties(null).getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DataTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DataTest.java
new file mode 100644
index 0000000..4e1f2b7
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DataTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class DataTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Data((Binary) null).toString());
+    }
+
+    @Test
+    public void testGetDataFromEmptySection() {
+        assertNull(new Data((byte[]) null).getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new Data((Binary) null).copy().getBinary());
+    }
+
+    @Test
+    public void testCopy() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+        Data data = new Data(binary);
+        Data copy = data.copy();
+
+        assertNotNull(copy.getValue());
+        assertNotSame(data.getValue(), copy.getValue());
+    }
+
+    @Test
+    public void testHashCode() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+        Data data = new Data(binary);
+        Data copy = data.copy();
+
+        assertNotNull(copy.getValue());
+        assertNotSame(data.getValue(), copy.getValue());
+
+        assertEquals(data.hashCode(), copy.hashCode());
+
+        Data second = new Data(new byte[] { 1, 2, 3 });
+
+        assertNotEquals(data.hashCode(), second.hashCode());
+
+        assertNotEquals(new Data((Binary) null).hashCode(), data.hashCode());
+        assertEquals(new Data((Binary) null).hashCode(), new Data((ProtonBuffer) null).hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+        Data data = new Data(binary);
+        Data copy = data.copy();
+
+        assertNotNull(copy.getValue());
+        assertNotSame(data.getValue(), copy.getValue());
+
+        assertEquals(data, data);
+        assertEquals(data, copy);
+
+        Data second = new Data(ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] { 1, 2, 3 }));
+        Data third = new Data(new byte[] { 1, 2, 3 }, 0, 3);
+        Data fourth = new Data(new byte[] { 1, 2, 3 }, 0, 1);
+        Data fifth = new Data(null, 0, 0);
+
+        assertNotEquals(data, second);
+        assertNotEquals(data, third);
+        assertNotEquals(data, fifth);
+        assertEquals(data, fourth);
+        assertFalse(data.equals(null));
+        assertNotEquals(data, "not a data");
+        assertNotEquals(data, new Data((Binary) null));
+        assertNotEquals(new Data((Binary) null), data);
+        assertEquals(new Data((Binary) null), new Data((ProtonBuffer) null));
+    }
+
+    @Test
+    public void testGetValueWhenUsingAnArrayView() {
+        Data view = new Data(new byte[] { 1, 2, 3 }, 0, 1);
+
+        assertArrayEquals(new byte[] {1}, view.getValue());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.Data, new Data((byte[]) null).getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnCloseTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnCloseTest.java
new file mode 100644
index 0000000..046cf0e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnCloseTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.Test;
+
+public class DeleteOnCloseTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(DeleteOnClose.getInstance().toString());
+    }
+
+    @Test
+    public void testSingleton() {
+        assertSame(DeleteOnClose.getInstance(), DeleteOnClose.getInstance());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksOrMessagesTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksOrMessagesTest.java
new file mode 100644
index 0000000..70705c6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksOrMessagesTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.Test;
+
+public class DeleteOnNoLinksOrMessagesTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(DeleteOnNoLinksOrMessages.getInstance().toString());
+    }
+
+    @Test
+    public void testSingleton() {
+        assertSame(DeleteOnNoLinksOrMessages.getInstance(), DeleteOnNoLinksOrMessages.getInstance());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksTest.java
new file mode 100644
index 0000000..443680d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoLinksTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.Test;
+
+public class DeleteOnNoLinksTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(DeleteOnNoLinks.getInstance().toString());
+    }
+
+    @Test
+    public void testSingleton() {
+        assertSame(DeleteOnNoLinks.getInstance(), DeleteOnNoLinks.getInstance());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoMessagesTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoMessagesTest.java
new file mode 100644
index 0000000..0bd2e5a
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeleteOnNoMessagesTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.Test;
+
+public class DeleteOnNoMessagesTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(DeleteOnNoMessages.getInstance().toString());
+    }
+
+    @Test
+    public void testSingleton() {
+        assertSame(DeleteOnNoMessages.getInstance(), DeleteOnNoMessages.getInstance());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeliveryAnnotationsTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeliveryAnnotationsTest.java
new file mode 100644
index 0000000..b63f11f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/DeliveryAnnotationsTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.junit.jupiter.api.Test;
+
+public class DeliveryAnnotationsTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new DeliveryAnnotations(null).toString());
+    }
+
+    @Test
+    public void testGetMapFromEmptySection() {
+        assertNull(new DeliveryAnnotations(null).getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new DeliveryAnnotations(null).copy().getValue());
+    }
+
+    @Test
+    public void testCopy() {
+        Map<Symbol, Object> payload = new HashMap<>();
+        payload.put(AmqpError.DECODE_ERROR, "value");
+
+        DeliveryAnnotations original = new DeliveryAnnotations(payload);
+        DeliveryAnnotations copy = original.copy();
+
+        assertNotSame(original, copy);
+        assertNotSame(original.getValue(), copy.getValue());
+        assertEquals(original.getValue(), copy.getValue());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.DeliveryAnnotations, new DeliveryAnnotations(null).getType());
+    }
+
+    @Test
+    public void testHashCode() {
+        Map<Symbol, Object> payload1 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key"), "value");
+
+        Map<Symbol, Object> payload2 = new HashMap<>();
+        payload2.put(Symbol.valueOf("key1"), "value");
+        payload2.put(Symbol.valueOf("key2"), "value");
+
+        DeliveryAnnotations original = new DeliveryAnnotations(payload1);
+        DeliveryAnnotations copy = original.copy();
+        DeliveryAnnotations another = new DeliveryAnnotations(payload2);
+
+        assertEquals(original.hashCode(), copy.hashCode());
+        assertNotEquals(original.hashCode(), another.hashCode());
+
+        DeliveryAnnotations empty = new DeliveryAnnotations(null);
+        DeliveryAnnotations empty2 = new DeliveryAnnotations(null);
+
+        assertEquals(empty2.hashCode(), empty.hashCode());
+        assertNotEquals(original.hashCode(), empty.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        Map<Symbol, Object> payload1 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key"), "value");
+
+        Map<Symbol, Object> payload2 = new HashMap<>();
+        payload2.put(Symbol.valueOf("key1"), "value");
+        payload2.put(Symbol.valueOf("key2"), "value");
+
+        DeliveryAnnotations original = new DeliveryAnnotations(payload1);
+        DeliveryAnnotations copy = original.copy();
+        DeliveryAnnotations another = new DeliveryAnnotations(payload2);
+        DeliveryAnnotations empty = new DeliveryAnnotations(null);
+        DeliveryAnnotations empty2 = new DeliveryAnnotations(null);
+
+        assertEquals(original, original);
+        assertEquals(original, copy);
+        assertNotEquals(original, another);
+        assertNotEquals(original, "test");
+        assertNotEquals(original, empty);
+        assertNotEquals(empty, original);
+        assertEquals(empty, empty2);
+
+        assertFalse(original.equals(null));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/FooterTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/FooterTest.java
new file mode 100644
index 0000000..0fae5c4
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/FooterTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class FooterTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Footer(null).toString());
+    }
+
+    @Test
+    public void testGetMapFromEmptySection() {
+        assertNull(new Footer(null).getValue());
+    }
+
+    @Test
+    public void testCopy() {
+        Map<Symbol, Object> payload = new HashMap<>();
+        payload.put(Symbol.valueOf("key"), "value");
+
+        Footer original = new Footer(payload);
+        Footer copy = original.copy();
+
+        assertNotSame(original, copy);
+        assertNotSame(original.getValue(), copy.getValue());
+        assertEquals(original.getValue(), copy.getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new Footer(null).copy().getValue());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.Footer, new Footer(null).getType());
+    }
+
+    @Test
+    public void testHashCode() {
+        Map<Symbol, Object> payload1 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key"), "value");
+
+        Map<Symbol, Object> payload2 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key1"), "value");
+        payload1.put(Symbol.valueOf("key2"), "value");
+
+        Footer original = new Footer(payload1);
+        Footer copy = original.copy();
+        Footer another = new Footer(payload2);
+
+        assertEquals(original.hashCode(), copy.hashCode());
+        assertNotEquals(original.hashCode(), another.hashCode());
+
+        Footer empty = new Footer(null);
+        Footer empty2 = new Footer(null);
+
+        assertEquals(empty2.hashCode(), empty.hashCode());
+        assertNotEquals(original.hashCode(), empty.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        Map<Symbol, Object> payload1 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key"), "value");
+
+        Map<Symbol, Object> payload2 = new HashMap<>();
+        payload2.put(Symbol.valueOf("key1"), "value");
+        payload2.put(Symbol.valueOf("key2"), "value");
+
+        Footer original = new Footer(payload1);
+        Footer copy = original.copy();
+        Footer another = new Footer(payload2);
+        Footer empty = new Footer(null);
+        Footer empty2 = new Footer(null);
+
+        assertEquals(original, original);
+        assertEquals(original, copy);
+        assertNotEquals(original, another);
+        assertNotEquals(original, "test");
+        assertNotEquals(original, empty);
+        assertNotEquals(empty, original);
+        assertEquals(empty, empty2);
+
+        assertFalse(original.equals(null));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/HeaderTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/HeaderTest.java
new file mode 100644
index 0000000..9bc15e9
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/HeaderTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class HeaderTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Header().toString());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.Header, new Header().getType());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Header header = new Header();
+
+        assertTrue(header.isEmpty());
+        header.setDurable(true);
+        assertFalse(header.isEmpty());
+        header.clearDurable();
+        assertTrue(header.isEmpty());
+        header.setDurable(false);
+        assertTrue(header.isEmpty());
+    }
+
+    @Test
+    public void testCreate() {
+        Header header = new Header();
+
+        assertFalse(header.hasDurable());
+        assertFalse(header.hasPriority());
+        assertFalse(header.hasTimeToLive());
+        assertFalse(header.hasFirstAcquirer());
+        assertFalse(header.hasDeliveryCount());
+        assertSame(header, header.getValue());
+
+        assertEquals(Header.DEFAULT_DURABILITY, header.isDurable());
+        assertEquals(Header.DEFAULT_PRIORITY, header.getPriority());
+        assertEquals(Header.DEFAULT_TIME_TO_LIVE, header.getTimeToLive());
+        assertEquals(Header.DEFAULT_FIRST_ACQUIRER, header.isFirstAcquirer());
+        assertEquals(Header.DEFAULT_DELIVERY_COUNT, header.getDeliveryCount());
+    }
+
+    @Test
+    public void testCopy() {
+        Header header = new Header();
+
+        header.setDurable(!Header.DEFAULT_DURABILITY);
+        header.setPriority((byte) (Header.DEFAULT_PRIORITY + 1));
+        header.setTimeToLive(Header.DEFAULT_TIME_TO_LIVE - 10);
+        header.setFirstAcquirer(!Header.DEFAULT_FIRST_ACQUIRER);
+        header.setDeliveryCount(Header.DEFAULT_DELIVERY_COUNT + 5);
+
+        Header copy = header.copy();
+
+        assertEquals(!Header.DEFAULT_DURABILITY, copy.isDurable());
+        assertEquals(Header.DEFAULT_PRIORITY + 1, copy.getPriority());
+        assertEquals(Header.DEFAULT_TIME_TO_LIVE - 10, copy.getTimeToLive());
+        assertEquals(!Header.DEFAULT_FIRST_ACQUIRER, copy.isFirstAcquirer());
+        assertEquals(Header.DEFAULT_DELIVERY_COUNT + 5, copy.getDeliveryCount());
+    }
+
+    @Test
+    public void testReset() {
+        Header header = new Header();
+
+        header.setDurable(!Header.DEFAULT_DURABILITY);
+        header.setPriority((byte) (Header.DEFAULT_PRIORITY + 1));
+        header.setTimeToLive(Header.DEFAULT_TIME_TO_LIVE - 10);
+        header.setFirstAcquirer(!Header.DEFAULT_FIRST_ACQUIRER);
+        header.setDeliveryCount(Header.DEFAULT_DELIVERY_COUNT + 5);
+
+        assertFalse(header.isEmpty());
+
+        header.reset();
+
+        assertTrue(header.isEmpty());
+    }
+
+    @Test
+    public void testClearDurable() {
+        Header header = new Header();
+
+        assertFalse(header.hasDurable());
+        assertEquals(Header.DEFAULT_DURABILITY, header.isDurable());
+
+        header.setDurable(!Header.DEFAULT_DURABILITY);
+        assertTrue(header.hasDurable());
+        assertNotEquals(Header.DEFAULT_DURABILITY, header.isDurable());
+
+        header.clearDurable();
+        assertFalse(header.hasDurable());
+        assertEquals(Header.DEFAULT_DURABILITY, header.isDurable());
+    }
+
+    @Test
+    public void testClearPriority() {
+        Header header = new Header();
+
+        assertFalse(header.hasPriority());
+        assertEquals(Header.DEFAULT_PRIORITY, header.getPriority());
+
+        header.setPriority((byte) (Header.DEFAULT_PRIORITY + 1));
+        assertTrue(header.hasPriority());
+        assertNotEquals(Header.DEFAULT_PRIORITY, header.getPriority());
+
+        header.clearPriority();
+        assertFalse(header.hasPriority());
+        assertEquals(Header.DEFAULT_PRIORITY, header.getPriority());
+
+        header.setPriority(Header.DEFAULT_PRIORITY);
+        assertFalse(header.hasPriority());
+        assertEquals(Header.DEFAULT_PRIORITY, header.getPriority());
+    }
+
+    @Test
+    public void testClearTimeToLive() {
+        Header header = new Header();
+
+        assertFalse(header.hasTimeToLive());
+        assertEquals(Header.DEFAULT_TIME_TO_LIVE, header.getTimeToLive());
+
+        header.setTimeToLive(Header.DEFAULT_TIME_TO_LIVE - 10);
+        assertTrue(header.hasTimeToLive());
+        assertNotEquals(Header.DEFAULT_TIME_TO_LIVE, header.getTimeToLive());
+
+        header.clearTimeToLive();
+        assertFalse(header.hasTimeToLive());
+        assertEquals(Header.DEFAULT_TIME_TO_LIVE, header.getTimeToLive());
+
+        header.setTimeToLive(0);
+        assertTrue(header.hasTimeToLive());
+        assertEquals(0, header.getTimeToLive());
+
+        header.setTimeToLive(UnsignedInteger.MAX_VALUE.intValue());
+        assertTrue(header.hasTimeToLive());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), header.getTimeToLive());
+
+        try {
+            header.setTimeToLive(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Should fail on out of range value");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        try {
+            header.setTimeToLive(-1l);
+            fail("Should fail on out of range value");
+        } catch (IllegalArgumentException iae) {
+        }
+    }
+
+    @Test
+    public void testClearFirstAcquirer() {
+        Header header = new Header();
+
+        assertFalse(header.hasFirstAcquirer());
+        assertEquals(Header.DEFAULT_FIRST_ACQUIRER, header.isFirstAcquirer());
+
+        header.setFirstAcquirer(!Header.DEFAULT_FIRST_ACQUIRER);
+        assertTrue(header.hasFirstAcquirer());
+        assertNotEquals(Header.DEFAULT_FIRST_ACQUIRER, header.isFirstAcquirer());
+
+        header.clearFirstAcquirer();
+        assertFalse(header.hasFirstAcquirer());
+        assertEquals(Header.DEFAULT_FIRST_ACQUIRER, header.isFirstAcquirer());
+    }
+
+    @Test
+    public void testClearDeliveryCount() {
+        Header header = new Header();
+
+        assertFalse(header.hasDeliveryCount());
+        assertEquals(Header.DEFAULT_DELIVERY_COUNT, header.getDeliveryCount());
+
+        header.setDeliveryCount(Header.DEFAULT_DELIVERY_COUNT + 10);
+        assertTrue(header.hasDeliveryCount());
+        assertNotEquals(Header.DEFAULT_DELIVERY_COUNT, header.getDeliveryCount());
+
+        header.clearDeliveryCount();
+        assertFalse(header.hasDeliveryCount());
+        assertEquals(Header.DEFAULT_DELIVERY_COUNT, header.getDeliveryCount());
+
+        header.setDeliveryCount(Header.DEFAULT_DELIVERY_COUNT);
+        assertFalse(header.hasDeliveryCount());
+        assertEquals(Header.DEFAULT_DELIVERY_COUNT, header.getDeliveryCount());
+
+        header.setDeliveryCount(UnsignedInteger.MAX_VALUE.intValue());
+        assertTrue(header.hasDeliveryCount());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), header.getDeliveryCount());
+
+        try {
+            header.setDeliveryCount(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Should fail on out of range value");
+        } catch (IllegalArgumentException iae) {
+        }
+
+        try {
+            header.setDeliveryCount(-1l);
+            fail("Should fail on out of range value");
+        } catch (IllegalArgumentException iae) {
+        }
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/MessageAnnotationsTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/MessageAnnotationsTest.java
new file mode 100644
index 0000000..6dad9d1
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/MessageAnnotationsTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.junit.jupiter.api.Test;
+
+public class MessageAnnotationsTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new MessageAnnotations(null).toString());
+    }
+
+    @Test
+    public void testGetSequenceFromEmptySection() {
+        assertNull(new MessageAnnotations(null).getValue());
+    }
+
+    @Test
+    public void testCopy() {
+        Map<Symbol, Object> payload = new HashMap<>();
+        payload.put(AmqpError.DECODE_ERROR, "value");
+
+        MessageAnnotations original = new MessageAnnotations(payload);
+        MessageAnnotations copy = original.copy();
+
+        assertNotSame(original, copy);
+        assertNotSame(original.getValue(), copy.getValue());
+        assertEquals(original.getValue(), copy.getValue());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new MessageAnnotations(null).copy().getValue());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.MessageAnnotations, new MessageAnnotations(null).getType());
+    }
+
+    @Test
+    public void testHashCode() {
+        Map<Symbol, Object> payload1 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key"), "value");
+
+        Map<Symbol, Object> payload2 = new HashMap<>();
+        payload2.put(Symbol.valueOf("key1"), "value");
+        payload2.put(Symbol.valueOf("key2"), "value");
+
+        MessageAnnotations original = new MessageAnnotations(payload1);
+        MessageAnnotations copy = original.copy();
+        MessageAnnotations another = new MessageAnnotations(payload2);
+
+        assertEquals(original.hashCode(), copy.hashCode());
+        assertNotEquals(original.hashCode(), another.hashCode());
+
+        MessageAnnotations empty = new MessageAnnotations(null);
+        MessageAnnotations empty2 = new MessageAnnotations(null);
+
+        assertEquals(empty2.hashCode(), empty.hashCode());
+        assertNotEquals(original.hashCode(), empty.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        Map<Symbol, Object> payload1 = new HashMap<>();
+        payload1.put(Symbol.valueOf("key"), "value");
+
+        Map<Symbol, Object> payload2 = new HashMap<>();
+        payload2.put(Symbol.valueOf("key1"), "value");
+        payload2.put(Symbol.valueOf("key2"), "value");
+
+        MessageAnnotations original = new MessageAnnotations(payload1);
+        MessageAnnotations copy = original.copy();
+        MessageAnnotations another = new MessageAnnotations(payload2);
+        MessageAnnotations empty = new MessageAnnotations(null);
+        MessageAnnotations empty2 = new MessageAnnotations(null);
+
+        assertEquals(original, original);
+        assertEquals(original, copy);
+        assertNotEquals(original, another);
+        assertNotEquals(original, "test");
+        assertNotEquals(original, empty);
+        assertNotEquals(empty, original);
+        assertEquals(empty, empty2);
+
+        assertFalse(original.equals(null));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ModifiedTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ModifiedTest.java
new file mode 100644
index 0000000..6b08e48
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ModifiedTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.junit.jupiter.api.Test;
+
+public class ModifiedTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Modified().toString());
+    }
+
+    @Test
+    public void testCreateWithDeliveryFailed() {
+        Modified modified = new Modified(true, false);
+        assertTrue(modified.isDeliveryFailed());
+        assertFalse(modified.isUndeliverableHere());
+        assertNull(modified.getMessageAnnotations());
+    }
+
+    @Test
+    public void testCreateWithDeliveryFailedAndUndeliverableHere() {
+        Modified modified = new Modified(true, true);
+        assertTrue(modified.isDeliveryFailed());
+        assertTrue(modified.isUndeliverableHere());
+        assertNull(modified.getMessageAnnotations());
+    }
+
+    @Test
+    public void testCreateWithDeliveryFailedAndUndeliverableHereAndAnnotations() {
+        Map<Symbol, Object> annotations = new HashMap<>();
+        annotations.put(Symbol.valueOf("key1"), "value");
+        annotations.put(Symbol.valueOf("key2"), "value");
+
+        Modified modified = new Modified(true, true, annotations);
+        assertTrue(modified.isDeliveryFailed());
+        assertTrue(modified.isUndeliverableHere());
+        assertNotNull(modified.getMessageAnnotations());
+
+        assertEquals(annotations, modified.getMessageAnnotations());
+    }
+
+    @Test
+    public void testAnnotations() {
+        Modified modified = new Modified();
+        assertNull(modified.getMessageAnnotations());
+        modified.setMessageAnnotations(new HashMap<Symbol, Object>());
+        assertNotNull(modified.getMessageAnnotations());
+    }
+
+    @Test
+    public void testDeliveryFailed() {
+        Modified modified = new Modified();
+        assertFalse(modified.isDeliveryFailed());
+        modified.setDeliveryFailed(true);
+        assertTrue(modified.isDeliveryFailed());
+    }
+
+    @Test
+    public void testUndeliverableHere() {
+        Modified modified = new Modified();
+        assertFalse(modified.isUndeliverableHere());
+        modified.setUndeliverableHere(true);
+        assertTrue(modified.isUndeliverableHere());
+    }
+
+    @Test
+    public void testGetAnnotationsFromEmptySection() {
+        assertNull(new Modified().getMessageAnnotations());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryStateType.Modified, new Modified().getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/PropertiesTypeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/PropertiesTypeTest.java
new file mode 100644
index 0000000..b5f0a26
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/PropertiesTypeTest.java
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.UUID;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.messaging.Section.SectionType;
+import org.junit.jupiter.api.Test;
+
+public class PropertiesTypeTest {
+
+    private static final String TEST_MESSAGE_ID = "test";
+    private static final Binary TEST_USER_ID = new Binary(new byte[] {1});
+    private static final String TEST_TO_ADDRESS = "to";
+    private static final String TEST_TO_SUBJECT = "subject";
+    private static final String TEST_REPLYTO_ADDRESS = "reply-to";
+    private static final String TEST_CORRELATION_ID = "correlation";
+    private static final String TEST_CONTENT_TYPE = "text/test";
+    private static final String TEST_CONTENT_ENCODING = "UTF-8";
+    private static final long TEST_ABSOLUTE_EXPIRY_TIME = 100l;
+    private static final long TEST_CREATION_TIME = 200l;
+    private static final String TEST_GROUP_ID = "group-test";
+    private static final long TEST_GROUP_SEQUENCE = 300l;
+    private static final String TEST_REPLYTO_GROUPID = "reply-to-group";
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Properties().toString());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SectionType.Properties, new Properties().getType());
+    }
+
+    @Test
+    public void testCreate() {
+        Properties properties = new Properties();
+
+        assertNull(properties.getMessageId());
+        assertNull(properties.getUserId());
+        assertNull(properties.getTo());
+        assertNull(properties.getSubject());
+        assertNull(properties.getReplyTo());
+        assertNull(properties.getCorrelationId());
+        assertNull(properties.getContentType());
+        assertNull(properties.getContentEncoding());
+        assertEquals(0, properties.getAbsoluteExpiryTime());
+        assertEquals(0, properties.getCreationTime());
+        assertNull(properties.getGroupId());
+        assertEquals(0, properties.getGroupSequence());
+        assertNull(properties.getReplyToGroupId());
+
+        assertTrue(properties.isEmpty());
+        assertEquals(0, properties.getElementCount());
+
+        assertSame(properties, properties.getValue());
+    }
+
+    @Test
+    public void testCopyFromDefault() {
+        Properties properties = new Properties();
+
+        assertNull(properties.getMessageId());
+        assertNull(properties.getUserId());
+        assertNull(properties.getTo());
+        assertNull(properties.getSubject());
+        assertNull(properties.getReplyTo());
+        assertNull(properties.getCorrelationId());
+        assertNull(properties.getContentType());
+        assertNull(properties.getContentEncoding());
+        assertEquals(0, properties.getAbsoluteExpiryTime());
+        assertEquals(0, properties.getCreationTime());
+        assertNull(properties.getGroupId());
+        assertEquals(0, properties.getGroupSequence());
+        assertNull(properties.getReplyToGroupId());
+        assertTrue(properties.isEmpty());
+        assertEquals(0, properties.getElementCount());
+
+        Properties copy = properties.copy();
+
+        assertNull(copy.getMessageId());
+        assertNull(copy.getUserId());
+        assertNull(copy.getTo());
+        assertNull(copy.getSubject());
+        assertNull(copy.getReplyTo());
+        assertNull(copy.getCorrelationId());
+        assertNull(copy.getContentType());
+        assertNull(copy.getContentEncoding());
+        assertEquals(0, copy.getAbsoluteExpiryTime());
+        assertEquals(0, copy.getCreationTime());
+        assertNull(copy.getGroupId());
+        assertEquals(0, copy.getGroupSequence());
+        assertNull(copy.getReplyToGroupId());
+        assertTrue(copy.isEmpty());
+        assertEquals(0, copy.getElementCount());
+    }
+
+    @Test
+    public void testCopyConstructor() {
+        Properties properties = new Properties();
+
+        properties.setMessageId(TEST_MESSAGE_ID);
+        properties.setUserId(TEST_USER_ID);
+        properties.setTo(TEST_TO_ADDRESS);
+        properties.setSubject(TEST_TO_SUBJECT);
+        properties.setReplyTo(TEST_REPLYTO_ADDRESS);
+        properties.setCorrelationId(TEST_CORRELATION_ID);
+        properties.setContentType(TEST_CONTENT_TYPE);
+        properties.setContentEncoding(TEST_CONTENT_ENCODING);
+        properties.setAbsoluteExpiryTime(TEST_ABSOLUTE_EXPIRY_TIME);
+        properties.setCreationTime(TEST_CREATION_TIME);
+        properties.setGroupId(TEST_GROUP_ID);
+        properties.setGroupSequence(TEST_GROUP_SEQUENCE);
+        properties.setReplyToGroupId(TEST_REPLYTO_GROUPID);
+
+        Properties copy = new Properties(properties);
+
+        assertFalse(copy.isEmpty());
+
+        assertTrue(copy.hasMessageId());
+        assertTrue(copy.hasUserId());
+        assertTrue(copy.hasTo());
+        assertTrue(copy.hasSubject());
+        assertTrue(copy.hasReplyTo());
+        assertTrue(copy.hasCorrelationId());
+        assertTrue(copy.hasContentType());
+        assertTrue(copy.hasContentEncoding());
+        assertTrue(copy.hasAbsoluteExpiryTime());
+        assertTrue(copy.hasCreationTime());
+        assertTrue(copy.hasGroupId());
+        assertTrue(copy.hasGroupSequence());
+        assertTrue(copy.hasReplyToGroupId());
+
+        // Check boolean has methods
+        assertEquals(properties.hasMessageId(), copy.hasMessageId());
+        assertEquals(properties.hasUserId(), copy.hasUserId());
+        assertEquals(properties.hasTo(), copy.hasTo());
+        assertEquals(properties.hasSubject(), copy.hasSubject());
+        assertEquals(properties.hasReplyTo(), copy.hasReplyTo());
+        assertEquals(properties.hasCorrelationId(), copy.hasCorrelationId());
+        assertEquals(properties.hasContentType(), copy.hasContentType());
+        assertEquals(properties.hasContentEncoding(), copy.hasContentEncoding());
+        assertEquals(properties.hasAbsoluteExpiryTime(), copy.hasAbsoluteExpiryTime());
+        assertEquals(properties.hasCreationTime(), copy.hasCreationTime());
+        assertEquals(properties.hasGroupId(), copy.hasGroupId());
+        assertEquals(properties.hasGroupSequence(), copy.hasGroupSequence());
+        assertEquals(properties.hasReplyToGroupId(), copy.hasReplyToGroupId());
+
+        // Test actual values copied
+        assertEquals(properties.getMessageId(), copy.getMessageId());
+        assertEquals(properties.getUserId(), copy.getUserId());
+        assertEquals(properties.getTo(), copy.getTo());
+        assertEquals(properties.getSubject(), copy.getSubject());
+        assertEquals(properties.getReplyTo(), copy.getReplyTo());
+        assertEquals(properties.getCorrelationId(), copy.getCorrelationId());
+        assertEquals(properties.getContentType(), copy.getContentType());
+        assertEquals(properties.getContentEncoding(), copy.getContentEncoding());
+        assertEquals(properties.getAbsoluteExpiryTime(), copy.getAbsoluteExpiryTime());
+        assertEquals(properties.getCreationTime(), copy.getCreationTime());
+        assertEquals(properties.getGroupId(), copy.getGroupId());
+        assertEquals(properties.getGroupSequence(), copy.getGroupSequence());
+        assertEquals(properties.getReplyToGroupId(), copy.getReplyToGroupId());
+
+        assertEquals(properties.getElementCount(), copy.getElementCount());
+    }
+
+    @Test
+    public void testGetElementCount() {
+        Properties properties = new Properties();
+
+        assertTrue(properties.isEmpty());
+        assertEquals(0, properties.getElementCount());
+
+        properties.setMessageId("ID");
+
+        assertFalse(properties.isEmpty());
+        assertEquals(1, properties.getElementCount());
+
+        properties.setMessageId(null);
+
+        assertTrue(properties.isEmpty());
+        assertEquals(0, properties.getElementCount());
+
+        properties.setReplyToGroupId("ID");
+        assertFalse(properties.isEmpty());
+        assertEquals(13, properties.getElementCount());
+
+        properties.setMessageId("ID");
+        assertFalse(properties.isEmpty());
+        assertEquals(13, properties.getElementCount());
+    }
+
+    @Test
+    public void testMessageId() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasMessageId());
+        assertNull(properties.getMessageId());
+
+        properties.setMessageId("ID");
+        assertTrue(properties.hasMessageId());
+        assertNotNull(properties.getMessageId());
+
+        properties.setMessageId(UUID.randomUUID());
+        assertTrue(properties.hasMessageId());
+        assertNotNull(properties.getMessageId());
+
+        properties.setMessageId(new Binary(new byte[] { 1 }));
+        assertTrue(properties.hasMessageId());
+        assertNotNull(properties.getMessageId());
+
+        properties.setMessageId(UnsignedLong.ZERO);
+        assertTrue(properties.hasMessageId());
+        assertNotNull(properties.getMessageId());
+
+        properties.setMessageId(null);
+        assertFalse(properties.hasMessageId());
+        assertNull(properties.getMessageId());
+
+        try {
+            properties.setMessageId(new HashMap<String, String>());
+            fail("Not a valid MessageId type.");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testUserId() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasUserId());
+        assertNull(properties.getUserId());
+
+        properties.setUserId(new Binary("ID".getBytes(StandardCharsets.UTF_8)));
+        assertTrue(properties.hasUserId());
+        assertNotNull(properties.getUserId());
+
+        properties.setUserId((Binary) null);
+        assertFalse(properties.hasUserId());
+        assertNull(properties.getUserId());
+    }
+
+    @Test
+    public void testUserIdFromByteArray() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasUserId());
+        assertNull(properties.getUserId());
+
+        properties.setUserId("ID".getBytes(StandardCharsets.UTF_8));
+        assertTrue(properties.hasUserId());
+        assertNotNull(properties.getUserId());
+
+        properties.setUserId((byte[]) null);
+        assertFalse(properties.hasUserId());
+        assertNull(properties.getUserId());
+    }
+
+    @Test
+    public void testTo() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasTo());
+        assertNull(properties.getTo());
+
+        properties.setTo("ID");
+        assertTrue(properties.hasTo());
+        assertNotNull(properties.getTo());
+
+        properties.setTo(null);
+        assertFalse(properties.hasTo());
+        assertNull(properties.getTo());
+    }
+
+    @Test
+    public void testSubject() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasSubject());
+        assertNull(properties.getSubject());
+
+        properties.setSubject("ID");
+        assertTrue(properties.hasSubject());
+        assertNotNull(properties.getSubject());
+
+        properties.setSubject(null);
+        assertFalse(properties.hasSubject());
+        assertNull(properties.getSubject());
+    }
+
+    @Test
+    public void testReplyTo() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasReplyTo());
+        assertNull(properties.getReplyTo());
+
+        properties.setReplyTo("ID");
+        assertTrue(properties.hasReplyTo());
+        assertNotNull(properties.getReplyTo());
+
+        properties.setReplyTo(null);
+        assertFalse(properties.hasReplyTo());
+        assertNull(properties.getReplyTo());
+    }
+
+    @Test
+    public void testCorrelationId() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasCorrelationId());
+        assertNull(properties.getCorrelationId());
+
+        properties.setCorrelationId("ID");
+        assertTrue(properties.hasCorrelationId());
+        assertNotNull(properties.getCorrelationId());
+
+        properties.setCorrelationId(null);
+        assertFalse(properties.hasCorrelationId());
+        assertNull(properties.getCorrelationId());
+    }
+
+    @Test
+    public void testContentType() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasContentType());
+        assertNull(properties.getContentType());
+
+        properties.setContentType("ID");
+        assertTrue(properties.hasContentType());
+        assertNotNull(properties.getContentType());
+
+        properties.setContentType(null);
+        assertFalse(properties.hasContentType());
+        assertNull(properties.getContentType());
+    }
+
+    @Test
+    public void testContentEncoding() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasContentEncoding());
+        assertNull(properties.getContentEncoding());
+
+        properties.setContentEncoding("ID");
+        assertTrue(properties.hasContentEncoding());
+        assertNotNull(properties.getContentEncoding());
+
+        properties.setContentEncoding(null);
+        assertFalse(properties.hasContentEncoding());
+        assertNull(properties.getContentEncoding());
+    }
+
+    @Test
+    public void testAbsoluteExpiryTime() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasAbsoluteExpiryTime());
+        assertEquals(0, properties.getAbsoluteExpiryTime());
+
+        properties.setAbsoluteExpiryTime(2048);
+        assertTrue(properties.hasAbsoluteExpiryTime());
+        assertEquals(2048, properties.getAbsoluteExpiryTime());
+
+        properties.clearAbsoluteExpiryTime();
+        assertFalse(properties.hasAbsoluteExpiryTime());
+        assertEquals(0, properties.getAbsoluteExpiryTime());
+    }
+
+    @Test
+    public void testCreationTime() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasCreationTime());
+        assertEquals(0, properties.getCreationTime());
+
+        properties.setCreationTime(2048);
+        assertTrue(properties.hasCreationTime());
+        assertEquals(2048, properties.getCreationTime());
+
+        properties.clearCreationTime();
+        assertFalse(properties.hasCreationTime());
+        assertEquals(0, properties.getCreationTime());
+    }
+
+    @Test
+    public void testGroupId() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasGroupId());
+        assertNull(properties.getGroupId());
+
+        properties.setGroupId("ID");
+        assertTrue(properties.hasGroupId());
+        assertNotNull(properties.getGroupId());
+
+        properties.setGroupId(null);
+        assertFalse(properties.hasGroupId());
+        assertNull(properties.getGroupId());
+    }
+
+    @Test
+    public void testGroupSequence() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasGroupSequence());
+        assertEquals(0, properties.getGroupSequence());
+
+        properties.setGroupSequence(2048);
+        assertTrue(properties.hasGroupSequence());
+        assertEquals(2048, properties.getGroupSequence());
+
+        properties.clearGroupSequence();
+        assertFalse(properties.hasGroupSequence());
+        assertEquals(0, properties.getGroupSequence());
+
+        properties.setGroupSequence(UnsignedInteger.MAX_VALUE.longValue());
+        assertTrue(properties.hasGroupSequence());
+        assertEquals(UnsignedInteger.MAX_VALUE.longValue(), properties.getGroupSequence());
+
+        try {
+            properties.setGroupSequence(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Should perform range check on set value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            properties.setGroupSequence(-1l);
+            fail("Should perform range check on set value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testReplyToGroupId() {
+        Properties properties = new Properties();
+
+        assertFalse(properties.hasReplyToGroupId());
+        assertNull(properties.getReplyToGroupId());
+
+        properties.setReplyToGroupId("ID");
+        assertTrue(properties.hasReplyToGroupId());
+        assertNotNull(properties.getReplyToGroupId());
+
+        properties.setReplyToGroupId(null);
+        assertFalse(properties.hasReplyToGroupId());
+        assertNull(properties.getReplyToGroupId());
+    }
+}
+
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ReceivedTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ReceivedTest.java
new file mode 100644
index 0000000..101f21a
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ReceivedTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.UnsignedLong;
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.junit.jupiter.api.Test;
+
+public class ReceivedTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Received().toString());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryStateType.Received, new Received().getType());
+    }
+
+    @Test
+    public void testSectionNumber() {
+        Received received = new Received();
+
+        assertNull(received.getSectionNumber());
+        received.setSectionNumber(UnsignedInteger.valueOf(20));
+        assertNotNull(received.getSectionNumber());
+    }
+
+    @Test
+    public void testSectionOffset() {
+        Received received = new Received();
+
+        assertNull(received.getSectionOffset());
+        received.setSectionOffset(UnsignedLong.valueOf(20));
+        assertNotNull(received.getSectionOffset());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/RejectedTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/RejectedTest.java
new file mode 100644
index 0000000..e29ec0e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/RejectedTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.qpid.protonj2.types.transport.AmqpError;
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.apache.qpid.protonj2.types.transport.ErrorCondition;
+import org.junit.jupiter.api.Test;
+
+public class RejectedTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Rejected().toString());
+    }
+
+    @Test
+    public void testGetErrorFromEmptySection() {
+        assertNull(new Rejected().getError());
+    }
+
+    @Test
+    public void testSetError() {
+        assertNull(new Rejected().getError());
+        ErrorCondition condition = new ErrorCondition(AmqpError.DECODE_ERROR, "Failed");
+        Rejected rejected = new Rejected();
+        rejected.setError(condition);
+        assertNotNull(condition);
+        assertSame(condition, rejected.getError());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryStateType.Rejected, new Rejected().getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ReleasedTypeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ReleasedTypeTest.java
new file mode 100644
index 0000000..b0aa2dc
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/ReleasedTypeTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.junit.jupiter.api.Test;
+
+public class ReleasedTypeTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(Released.getInstance().toString());
+    }
+
+    @Test
+    public void testSingleton() {
+        assertSame(Released.getInstance(), Released.getInstance());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryStateType.Released, Released.getInstance().getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/SourceTypeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/SourceTypeTest.java
new file mode 100644
index 0000000..97bc380
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/SourceTypeTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+public class SourceTypeTest {
+
+    @Test
+    public void testSetExpiryPolicy() {
+        Source source = new Source();
+
+        assertEquals(TerminusExpiryPolicy.SESSION_END, source.getExpiryPolicy());
+        source.setExpiryPolicy(TerminusExpiryPolicy.CONNECTION_CLOSE);
+        assertEquals(TerminusExpiryPolicy.CONNECTION_CLOSE, source.getExpiryPolicy());
+        source.setExpiryPolicy(null);
+        assertEquals(TerminusExpiryPolicy.SESSION_END, source.getExpiryPolicy());
+    }
+
+    @Test
+    public void testTerminusDurability() {
+        Source source = new Source();
+
+        assertEquals(TerminusDurability.NONE, source.getDurable());
+        source.setDurable(TerminusDurability.UNSETTLED_STATE);
+        assertEquals(TerminusDurability.UNSETTLED_STATE, source.getDurable());
+        source.setDurable(null);
+        assertEquals(TerminusDurability.NONE, source.getDurable());
+    }
+
+    @Test
+    public void testCreate() {
+        Source source = new Source();
+
+        assertNull(source.getAddress());
+        assertEquals(TerminusDurability.NONE, source.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, source.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, source.getTimeout());
+        assertFalse(source.isDynamic());
+        assertNull(source.getDynamicNodeProperties());
+        assertNull(source.getDistributionMode());
+        assertNull(source.getFilter());
+        assertNull(source.getDefaultOutcome());
+        assertNull(source.getOutcomes());
+        assertNull(source.getCapabilities());
+    }
+
+    @Test
+    public void testCopyFromDefault() {
+        Source source = new Source();
+
+        assertNull(source.getAddress());
+        assertEquals(TerminusDurability.NONE, source.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, source.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, source.getTimeout());
+        assertFalse(source.isDynamic());
+        assertNull(source.getDynamicNodeProperties());
+        assertNull(source.getDistributionMode());
+        assertNull(source.getFilter());
+        assertNull(source.getDefaultOutcome());
+        assertNull(source.getOutcomes());
+        assertNull(source.getCapabilities());
+
+        Source copy = source.copy();
+
+        assertNull(copy.getAddress());
+        assertEquals(TerminusDurability.NONE, copy.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, copy.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, copy.getTimeout());
+        assertFalse(copy.isDynamic());
+        assertNull(copy.getDynamicNodeProperties());
+        assertNull(copy.getDistributionMode());
+        assertNull(copy.getFilter());
+        assertNull(copy.getDefaultOutcome());
+        assertNull(copy.getOutcomes());
+        assertNull(copy.getCapabilities());
+    }
+
+    @Test
+    public void testCopyWithValues() {
+        Source source = new Source();
+
+        Map<Symbol, Object> dynamicProperties = new HashMap<>();
+        dynamicProperties.put(Symbol.valueOf("test"), "test");
+        Map<Symbol, Object> filter = new HashMap<>();
+        filter.put(Symbol.valueOf("filter"), "filter");
+
+        assertNull(source.getAddress());
+        assertEquals(TerminusDurability.NONE, source.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, source.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, source.getTimeout());
+        assertFalse(source.isDynamic());
+        source.setDynamicNodeProperties(dynamicProperties);
+        assertNotNull(source.getDynamicNodeProperties());
+        assertNull(source.getDistributionMode());
+        source.setFilter(filter);
+        assertNotNull(source.getFilter());
+        assertNull(source.getDefaultOutcome());
+        source.setOutcomes(Symbol.valueOf("accepted"));
+        assertNotNull(source.getOutcomes());
+        source.setCapabilities(Symbol.valueOf("test"));
+        assertNotNull(source.getCapabilities());
+
+        Source copy = source.copy();
+
+        assertNull(copy.getAddress());
+        assertEquals(TerminusDurability.NONE, copy.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, copy.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, copy.getTimeout());
+        assertFalse(copy.isDynamic());
+        assertNotNull(copy.getDynamicNodeProperties());
+        assertEquals(dynamicProperties, copy.getDynamicNodeProperties());
+        assertNull(copy.getDistributionMode());
+        assertNotNull(copy.getFilter());
+        assertEquals(filter, copy.getFilter());
+        assertNull(copy.getDefaultOutcome());
+        assertNotNull(copy.getOutcomes());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("accepted") }, source.getOutcomes());
+        assertNotNull(copy.getCapabilities());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("test") }, source.getCapabilities());
+
+        assertEquals(source.toString(), copy.toString());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Source().toString());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TargetTypeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TargetTypeTest.java
new file mode 100644
index 0000000..06951f6
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TargetTypeTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+public class TargetTypeTest {
+
+    @Test
+    public void testCreate() {
+        Target target = new Target();
+
+        assertNull(target.getAddress());
+        assertEquals(TerminusDurability.NONE, target.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, target.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, target.getTimeout());
+        assertFalse(target.isDynamic());
+        assertNull(target.getDynamicNodeProperties());
+        assertNull(target.getCapabilities());
+    }
+
+    @Test
+    public void testCopyFromDefault() {
+        Target target = new Target();
+
+        assertNull(target.getAddress());
+        assertEquals(TerminusDurability.NONE, target.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, target.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, target.getTimeout());
+        assertFalse(target.isDynamic());
+        assertNull(target.getDynamicNodeProperties());
+        assertNull(target.getCapabilities());
+
+        Target copy = target.copy();
+
+        assertNull(copy.getAddress());
+        assertEquals(TerminusDurability.NONE, copy.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, copy.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, copy.getTimeout());
+        assertFalse(copy.isDynamic());
+        assertNull(copy.getDynamicNodeProperties());
+        assertNull(copy.getCapabilities());
+    }
+
+    @Test
+    public void testCopyWithValues() {
+        Target target = new Target();
+
+        Map<Symbol, Object> dynamicProperties = new HashMap<>();
+        dynamicProperties.put(Symbol.valueOf("test"), "test");
+
+        assertNull(target.getAddress());
+        assertEquals(TerminusDurability.NONE, target.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, target.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, target.getTimeout());
+        assertFalse(target.isDynamic());
+        target.setDynamicNodeProperties(dynamicProperties);
+        assertNotNull(target.getDynamicNodeProperties());
+        target.setCapabilities(Symbol.valueOf("test"));
+        assertNotNull(target.getCapabilities());
+
+        Target copy = target.copy();
+
+        assertNull(copy.getAddress());
+        assertEquals(TerminusDurability.NONE, copy.getDurable());
+        assertEquals(TerminusExpiryPolicy.SESSION_END, copy.getExpiryPolicy());
+        assertEquals(UnsignedInteger.ZERO, copy.getTimeout());
+        assertFalse(copy.isDynamic());
+        assertNotNull(copy.getDynamicNodeProperties());
+        assertEquals(dynamicProperties, copy.getDynamicNodeProperties());
+        assertNotNull(copy.getCapabilities());
+        assertArrayEquals(new Symbol[] { Symbol.valueOf("test") }, target.getCapabilities());
+
+        assertEquals(target.toString(), copy.toString());
+    }
+
+    @Test
+    public void testSetExpiryPolicy() {
+        Target target = new Target();
+
+        assertEquals(TerminusExpiryPolicy.SESSION_END, target.getExpiryPolicy());
+        target.setExpiryPolicy(TerminusExpiryPolicy.CONNECTION_CLOSE);
+        assertEquals(TerminusExpiryPolicy.CONNECTION_CLOSE, target.getExpiryPolicy());
+        target.setExpiryPolicy(null);
+        assertEquals(TerminusExpiryPolicy.SESSION_END, target.getExpiryPolicy());
+    }
+
+    @Test
+    public void testTerminusDurability() {
+        Target target = new Target();
+
+        assertEquals(TerminusDurability.NONE, target.getDurable());
+        target.setDurable(TerminusDurability.UNSETTLED_STATE);
+        assertEquals(TerminusDurability.UNSETTLED_STATE, target.getDurable());
+        target.setDurable(null);
+        assertEquals(TerminusDurability.NONE, target.getDurable());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Target().toString());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TerminusDurabilityTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TerminusDurabilityTest.java
new file mode 100644
index 0000000..48d4db8
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TerminusDurabilityTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+public class TerminusDurabilityTest {
+
+    @Test
+    public void testValueOf() {
+        assertEquals(TerminusDurability.valueOf(0l), TerminusDurability.NONE);
+        assertEquals(TerminusDurability.valueOf(1l), TerminusDurability.CONFIGURATION);
+        assertEquals(TerminusDurability.valueOf(2l), TerminusDurability.UNSETTLED_STATE);
+
+        try {
+            TerminusDurability.valueOf(100);
+            fail("Should not accept null value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            TerminusDurability.valueOf(-1);
+            fail("Should not accept null value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testValueOfUnsignedInteger() {
+        assertEquals(TerminusDurability.valueOf(UnsignedInteger.valueOf(0)), TerminusDurability.NONE);
+        assertEquals(TerminusDurability.valueOf(UnsignedInteger.valueOf(1)), TerminusDurability.CONFIGURATION);
+        assertEquals(TerminusDurability.valueOf(UnsignedInteger.valueOf(2)), TerminusDurability.UNSETTLED_STATE);
+
+        try {
+            TerminusDurability.valueOf(UnsignedInteger.valueOf(22));
+            fail("Should not accept null value");
+        } catch (IllegalArgumentException iae) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TerminusExpiryPolicyTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TerminusExpiryPolicyTest.java
new file mode 100644
index 0000000..4a5556e
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/messaging/TerminusExpiryPolicyTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class TerminusExpiryPolicyTest {
+
+    @Test
+    public void testValueOf() {
+        assertEquals(TerminusExpiryPolicy.valueOf(Symbol.valueOf("link-detach")), TerminusExpiryPolicy.LINK_DETACH);
+        assertEquals(TerminusExpiryPolicy.valueOf(Symbol.valueOf("session-end")), TerminusExpiryPolicy.SESSION_END);
+        assertEquals(TerminusExpiryPolicy.valueOf(Symbol.valueOf("connection-close")), TerminusExpiryPolicy.CONNECTION_CLOSE);
+        assertEquals(TerminusExpiryPolicy.valueOf(Symbol.valueOf("never")), TerminusExpiryPolicy.NEVER);
+
+        try {
+            TerminusExpiryPolicy.valueOf((Symbol) null);
+            fail("Should not accept null value");
+        } catch (IllegalArgumentException iae) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslChallengeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslChallengeTest.java
new file mode 100644
index 0000000..9fe8acb
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslChallengeTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeType;
+import org.junit.jupiter.api.Test;
+
+public class SaslChallengeTest {
+
+    @Test
+    public void testInvoke() {
+        AtomicReference<String> result = new AtomicReference<>();
+        new SaslChallenge().invoke(new SaslPerformativeHandler<String>() {
+
+            @Override
+            public void handleChallenge(SaslChallenge saslChallenge, String context) {
+                result.set(context);
+            }
+        }, "test");
+
+        assertEquals("test", result.get());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new SaslChallenge().toString());
+    }
+
+    @Test
+    public void testGetDataFromEmptySection() {
+        assertNull(new SaslChallenge().getChallenge());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new SaslChallenge().copy().getChallenge());
+    }
+
+    @Test
+    public void testMechanismRequired() {
+        SaslChallenge init = new SaslChallenge();
+
+        try {
+            init.setChallenge((Binary) null);
+            fail("Challenge field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+
+        try {
+            init.setChallenge((ProtonBuffer) null);
+            fail("Challenge field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testCopy() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+
+        SaslChallenge value = new SaslChallenge();
+
+        value.setChallenge(binary);
+
+        SaslChallenge copy = value.copy();
+
+        assertNotSame(copy, value);
+        assertArrayEquals(value.getChallenge().getArray(), copy.getChallenge().getArray());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SaslPerformativeType.CHALLENGE, new SaslChallenge().getPerformativeType());
+    }
+
+    @Test
+    public void testPerformativeHandlerInvocations() {
+        final SaslChallenge value = new SaslChallenge();
+        final AtomicBoolean invoked = new AtomicBoolean();
+
+        value.invoke(new SaslPerformativeHandler<AtomicBoolean>() {
+
+            @Override
+            public void handleChallenge(SaslChallenge saslChallenge, AtomicBoolean context) {
+                context.set(true);
+            }
+
+        }, invoked);
+
+        assertTrue(invoked.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslInitTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslInitTest.java
new file mode 100644
index 0000000..65df0a4
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslInitTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeType;
+import org.junit.jupiter.api.Test;
+
+public class SaslInitTest {
+
+    @Test
+    public void testInvoke() {
+        AtomicReference<String> result = new AtomicReference<>();
+        new SaslInit().invoke(new SaslPerformativeHandler<String>() {
+
+            @Override
+            public void handleInit(SaslInit saslInit, String context) {
+                result.set(context);
+            }
+        }, "test");
+
+        assertEquals("test", result.get());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new SaslInit().toString());
+    }
+
+    @Test
+    public void testGetDataFromEmptySection() {
+        assertNull(new SaslInit().getHostname());
+        assertNull(new SaslInit().getInitialResponse());
+        assertNull(new SaslInit().getMechanism());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new SaslInit().copy().getHostname());
+    }
+
+    @Test
+    public void testMechanismRequired() {
+        SaslInit init = new SaslInit();
+
+        try {
+            init.setMechanism(null);
+            fail("Mechanism field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testCopy() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+
+        SaslInit init = new SaslInit();
+
+        init.setHostname("localhost");
+        init.setInitialResponse(binary);
+        init.setMechanism(Symbol.valueOf("ANONYMOUS"));
+
+        SaslInit copy = init.copy();
+
+        assertNotSame(copy, init);
+        assertEquals(init.getHostname(), copy.getHostname());
+        assertArrayEquals(init.getInitialResponse().getArray(), copy.getInitialResponse().getArray());
+        assertEquals(init.getMechanism(), copy.getMechanism());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SaslPerformativeType.INIT, new SaslInit().getPerformativeType());
+    }
+
+    @Test
+    public void testPerformativeHandlerInvocations() {
+        final SaslInit value = new SaslInit();
+        final AtomicBoolean invoked = new AtomicBoolean();
+
+        value.invoke(new SaslPerformativeHandler<AtomicBoolean>() {
+
+            @Override
+            public void handleInit(SaslInit saslInit, AtomicBoolean context) {
+                context.set(true);
+            }
+
+        }, invoked);
+
+        assertTrue(invoked.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslMechanismsTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslMechanismsTest.java
new file mode 100644
index 0000000..80e4d38
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslMechanismsTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeType;
+import org.junit.jupiter.api.Test;
+
+public class SaslMechanismsTest {
+
+    @Test
+    public void testInvoke() {
+        AtomicReference<String> result = new AtomicReference<>();
+        new SaslMechanisms().invoke(new SaslPerformativeHandler<String>() {
+
+            @Override
+            public void handleMechanisms(SaslMechanisms saslMechanisms, String context) {
+                result.set(context);
+            }
+        }, "test");
+
+        assertEquals("test", result.get());
+    }
+
+    @Test
+    public void testToStringOnNonEmptyObject() {
+        Symbol[] mechanisms = new Symbol[] { Symbol.valueOf("EXTERNAL"), Symbol.valueOf("PLAIN") };
+        SaslMechanisms value = new SaslMechanisms();
+
+        value.setSaslServerMechanisms(mechanisms);
+
+        assertNotNull(value.toString());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new SaslMechanisms().toString());
+    }
+
+    @Test
+    public void testGetDataFromEmptySection() {
+        assertNull(new SaslMechanisms().getSaslServerMechanisms());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new SaslMechanisms().copy().getSaslServerMechanisms());
+    }
+
+    @Test
+    public void testMechanismRequired() {
+        SaslMechanisms init = new SaslMechanisms();
+
+        try {
+            init.setSaslServerMechanisms((Symbol[]) null);
+            fail("Server Mechanisms field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testCopy() {
+        Symbol[] mechanisms = new Symbol[] { Symbol.valueOf("EXTERNAL"), Symbol.valueOf("PLAIN") };
+        SaslMechanisms value = new SaslMechanisms();
+
+        value.setSaslServerMechanisms(mechanisms);
+
+        SaslMechanisms copy = value.copy();
+
+        assertNotSame(copy, value);
+        assertArrayEquals(value.getSaslServerMechanisms(), copy.getSaslServerMechanisms());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SaslPerformativeType.MECHANISMS, new SaslMechanisms().getPerformativeType());
+    }
+
+    @Test
+    public void testPerformativeHandlerInvocations() {
+        final SaslMechanisms value = new SaslMechanisms();
+        final AtomicBoolean invoked = new AtomicBoolean();
+
+        value.invoke(new SaslPerformativeHandler<AtomicBoolean>() {
+
+            @Override
+            public void handleMechanisms(SaslMechanisms saslMechanisms, AtomicBoolean context) {
+                context.set(true);
+            }
+
+        }, invoked);
+
+        assertTrue(invoked.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslOutcomeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslOutcomeTest.java
new file mode 100644
index 0000000..30b50fe
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslOutcomeTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeType;
+import org.junit.jupiter.api.Test;
+
+public class SaslOutcomeTest {
+
+    @Test
+    public void testInvoke() {
+        AtomicReference<String> result = new AtomicReference<>();
+        new SaslOutcome().invoke(new SaslPerformativeHandler<String>() {
+
+            @Override
+            public void handleOutcome(SaslOutcome saslOutcome, String context) {
+                result.set(context);
+            }
+        }, "test");
+
+        assertEquals("test", result.get());
+    }
+
+    @Test
+    public void testToStringOnNonEmptyObject() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+
+        SaslOutcome value = new SaslOutcome();
+
+        value.setCode(SaslCode.OK);
+        value.setAdditionalData(binary);
+
+        assertNotNull(value.toString());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new SaslOutcome().toString());
+    }
+
+    @Test
+    public void testGetDataFromEmptySection() {
+        assertNull(new SaslOutcome().getCode());
+        assertNull(new SaslOutcome().getAdditionalData());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new SaslOutcome().copy().getCode());
+    }
+
+    @Test
+    public void testMechanismRequired() {
+        SaslOutcome init = new SaslOutcome();
+
+        try {
+            init.setCode(null);
+            fail("Outcome field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testCopy() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+
+        SaslOutcome value = new SaslOutcome();
+
+        value.setCode(SaslCode.OK);
+        value.setAdditionalData(binary);
+
+        SaslOutcome copy = value.copy();
+
+        assertNotSame(copy, value);
+        assertEquals(copy.getCode(), value.getCode());
+        assertArrayEquals(value.getAdditionalData().getArray(), copy.getAdditionalData().getArray());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SaslPerformativeType.OUTCOME, new SaslOutcome().getPerformativeType());
+    }
+
+    @Test
+    public void testPerformativeHandlerInvocations() {
+        final SaslOutcome value = new SaslOutcome();
+        final AtomicBoolean invoked = new AtomicBoolean();
+
+        value.invoke(new SaslPerformativeHandler<AtomicBoolean>() {
+
+            @Override
+            public void handleOutcome(SaslOutcome saslOutcome, AtomicBoolean context) {
+                context.set(true);
+            }
+
+        }, invoked);
+
+        assertTrue(invoked.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslResponseTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslResponseTest.java
new file mode 100644
index 0000000..c02235a
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/security/SaslResponseTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeHandler;
+import org.apache.qpid.protonj2.types.security.SaslPerformative.SaslPerformativeType;
+import org.junit.jupiter.api.Test;
+
+public class SaslResponseTest {
+
+    @Test
+    public void testInvoke() {
+        AtomicReference<String> result = new AtomicReference<>();
+        new SaslResponse().invoke(new SaslPerformativeHandler<String>() {
+
+            @Override
+            public void handleResponse(SaslResponse saslResponse, String context) {
+                result.set(context);
+            }
+        }, "test");
+
+        assertEquals("test", result.get());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new SaslResponse().toString());
+    }
+
+    @Test
+    public void testGetDataFromEmptySection() {
+        assertNull(new SaslResponse().getResponse());
+    }
+
+    @Test
+    public void testCopyFromEmpty() {
+        assertNull(new SaslResponse().copy().getResponse());
+    }
+
+    @Test
+    public void testMechanismRequired() {
+        SaslResponse init = new SaslResponse();
+
+        try {
+            init.setResponse((Binary) null);
+            fail("Response field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+
+        try {
+            init.setResponse((ProtonBuffer) null);
+            fail("Response field is required and should not be cleared");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testCopy() {
+        byte[] bytes = new byte[] { 1 };
+        Binary binary = new Binary(bytes);
+
+        SaslResponse value = new SaslResponse();
+
+        value.setResponse(binary);
+
+        SaslResponse copy = value.copy();
+
+        assertNotSame(copy, value);
+        assertArrayEquals(value.getResponse().getArray(), copy.getResponse().getArray());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(SaslPerformativeType.RESPONSE, new SaslResponse().getPerformativeType());
+    }
+
+    @Test
+    public void testPerformativeHandlerInvocations() {
+        final SaslResponse value = new SaslResponse();
+        final AtomicBoolean invoked = new AtomicBoolean();
+
+        value.invoke(new SaslPerformativeHandler<AtomicBoolean>() {
+
+            @Override
+            public void handleResponse(SaslResponse saslResponse, AtomicBoolean context) {
+                context.set(true);
+            }
+
+        }, invoked);
+
+        assertTrue(invoked.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/CoordinatorTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/CoordinatorTest.java
new file mode 100644
index 0000000..5d3d353
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/CoordinatorTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class CoordinatorTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Coordinator().toString());
+    }
+
+    @Test
+    public void testCopyOnEmpty() {
+        assertNotNull(new Coordinator().copy());
+    }
+
+    @Test
+    public void testCopy() {
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+
+        Coordinator copy = coordinator.copy();
+
+        assertNotSame(copy.getCapabilities(), coordinator.getCapabilities());
+        assertArrayEquals(copy.getCapabilities(), coordinator.getCapabilities());
+
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN, TxnCapability.PROMOTABLE_TXN);
+
+        copy = coordinator.copy();
+
+        assertNotSame(copy.getCapabilities(), coordinator.getCapabilities());
+        assertArrayEquals(copy.getCapabilities(), coordinator.getCapabilities());
+    }
+
+    @Test
+    public void testCapabilities() {
+        Coordinator coordinator = new Coordinator();
+
+        assertNull(coordinator.getCapabilities());
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        assertNotNull(coordinator.getCapabilities());
+        assertNotNull(coordinator.toString());
+
+        assertArrayEquals(new Symbol[] { TxnCapability.LOCAL_TXN }, coordinator.getCapabilities());
+    }
+
+    @Test
+    public void testToStringWithCapabilities() {
+        Coordinator coordinator = new Coordinator();
+        coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+        assertNotNull(new Coordinator().toString());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DeclareTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DeclareTest.java
new file mode 100644
index 0000000..b9be69a
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DeclareTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+public class DeclareTest {
+
+    @Test
+    public void testGlobalTxnId() {
+        Declare declare = new Declare();
+
+        assertNull(declare.getGlobalId());
+        declare.setGlobalId(new GlobalTxId() {});
+        assertNotNull(declare.getGlobalId());
+    }
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Declare().toString());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DeclaredTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DeclaredTest.java
new file mode 100644
index 0000000..fd522bf
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DeclaredTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.junit.jupiter.api.Test;
+
+public class DeclaredTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Declared().toString());
+    }
+
+    @Test
+    public void testTxnId() {
+        Binary txnId = new Binary(new byte[] { 1 });
+        Declared declare = new Declared();
+
+        assertNull(declare.getTxnId());
+        declare.setTxnId(txnId);
+        assertNotNull(declare.getTxnId());
+
+        try {
+            declare.setTxnId(null);
+            fail("The TXN field is mandatory and cannot be set to null");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryStateType.Declared, new Declared().getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DischargeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DischargeTest.java
new file mode 100644
index 0000000..afbd9cb
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/DischargeTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.junit.jupiter.api.Test;
+
+public class DischargeTest {
+
+    @Test
+    public void testToStringOnEmptyObject() {
+        assertNotNull(new Discharge().toString());
+    }
+
+    @Test
+    public void testTxnId() {
+        Binary txnId = new Binary(new byte[] { 1 });
+        Discharge discharge = new Discharge();
+
+        assertNull(discharge.getTxnId());
+        discharge.setTxnId(txnId);
+        assertNotNull(discharge.getTxnId());
+
+        try {
+            discharge.setTxnId(null);
+            fail("The TXN field is mandatory and cannot be set to null");
+        } catch (NullPointerException npe) {}
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/TransactionalStateTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/TransactionalStateTest.java
new file mode 100644
index 0000000..9e1b23d
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transactions/TransactionalStateTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transactions;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.transport.DeliveryState.DeliveryStateType;
+import org.junit.jupiter.api.Test;
+
+public class TransactionalStateTest {
+
+    @Test
+    public void testToString() {
+        assertNotNull(new TransactionalState().toString());
+    }
+
+    @Test
+    public void testTxnId() {
+        Binary txnId = new Binary(new byte[] { 1 });
+        TransactionalState state = new TransactionalState();
+
+        assertNull(state.getTxnId());
+        state.setTxnId(txnId);
+        assertNotNull(state.getTxnId());
+
+        try {
+            state.setTxnId(null);
+            fail("The TXN field is mandatory and cannot be set to null");
+        } catch (NullPointerException npe) {}
+    }
+
+    @Test
+    public void testOutcome() {
+        TransactionalState state = new TransactionalState();
+
+        assertNull(state.getOutcome());
+        state.setOutcome(Accepted.getInstance());
+        assertNotNull(state.getOutcome());
+    }
+
+    @Test
+    public void testGetType() {
+        assertEquals(DeliveryStateType.Transactional, new TransactionalState().getType());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/AMQPHeaderTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/AMQPHeaderTest.java
new file mode 100644
index 0000000..c9af204
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/AMQPHeaderTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonByteBufferAllocator;
+import org.apache.qpid.protonj2.types.transport.AMQPHeader.HeaderHandler;
+import org.junit.jupiter.api.Test;
+
+public class AMQPHeaderTest {
+
+    @Test
+    public void testDefaultCreate() {
+        AMQPHeader header = new AMQPHeader();
+
+        assertEquals(AMQPHeader.getAMQPHeader(), header);
+        assertFalse(header.isSaslHeader());
+        assertEquals(0, header.getProtocolId());
+        assertEquals(1, header.getMajor());
+        assertEquals(0, header.getMinor());
+        assertEquals(0, header.getRevision());
+        assertTrue(header.hasValidPrefix());
+    }
+
+    @Test
+    public void testToArray() {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader header = new AMQPHeader(buffer);
+        byte[] array = header.toArray();
+
+        assertArrayEquals(buffer.getArray(), array);
+    }
+
+    @Test
+    public void testToByteBuffer() {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader header = new AMQPHeader(buffer);
+        ByteBuffer byteBuffer = header.toByteBuffer();
+
+        assertArrayEquals(buffer.getArray(), byteBuffer.array());
+    }
+
+    @Test
+    public void testCreateFromBufferWithoutValidation() {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {'A', 'M', 'Q', 'P', 4, 1, 0, 0});
+        AMQPHeader invalid = new AMQPHeader(buffer, false);
+
+        assertEquals(4, invalid.getByteAt(4));
+        assertEquals(4, invalid.getProtocolId());
+    }
+
+    @Test
+    public void testCreateFromBufferWithoutValidationFailsWithToLargeInput() {
+        ProtonBuffer buffer = ProtonByteBufferAllocator.DEFAULT.wrap(new byte[] {'A', 'M', 'Q', 'P', 4, 1, 0, 0, 0});
+        assertThrows(IndexOutOfBoundsException.class, () -> new AMQPHeader(buffer, false));
+    }
+
+    @Test
+    public void testGetBuffer() {
+        AMQPHeader header = new AMQPHeader();
+
+        assertNotNull(header.getBuffer());
+        ProtonBuffer buffer = header.getBuffer();
+
+        buffer.setByte(0, 'B');
+
+        assertEquals('A', header.getByteAt(0));
+    }
+
+    @Test
+    public void testHashCode() {
+        AMQPHeader defaultCtor = new AMQPHeader();
+        AMQPHeader byteCtor = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader byteCtorSasl = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 3, 1, 0, 0});
+
+        assertEquals(defaultCtor.hashCode(), byteCtor.hashCode());
+        assertEquals(defaultCtor.hashCode(), AMQPHeader.getAMQPHeader().hashCode());
+        assertEquals(byteCtor.hashCode(), AMQPHeader.getAMQPHeader().hashCode());
+        assertEquals(byteCtorSasl.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+        assertNotEquals(byteCtor.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+        assertNotEquals(defaultCtor.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+        assertEquals(byteCtorSasl.hashCode(), AMQPHeader.getSASLHeader().hashCode());
+    }
+
+    @Test
+    public void testIsTypeMethods() {
+        AMQPHeader defaultCtor = new AMQPHeader();
+        AMQPHeader byteCtor = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader byteCtorSasl = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 3, 1, 0, 0});
+
+        assertFalse(defaultCtor.isSaslHeader());
+        assertFalse(byteCtor.isSaslHeader());
+        assertTrue(byteCtorSasl.isSaslHeader());
+        assertFalse(AMQPHeader.getAMQPHeader().isSaslHeader());
+        assertTrue(AMQPHeader.getSASLHeader().isSaslHeader());
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testEquals() {
+        AMQPHeader defaultCtor = new AMQPHeader();
+        AMQPHeader byteCtor = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 0});
+        AMQPHeader byteCtorSasl = new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 3, 1, 0, 0});
+
+        assertEquals(defaultCtor, defaultCtor);
+        assertEquals(defaultCtor, byteCtor);
+        assertEquals(byteCtor, byteCtor);
+        assertEquals(defaultCtor, AMQPHeader.getAMQPHeader());
+        assertEquals(byteCtor, AMQPHeader.getAMQPHeader());
+        assertEquals(byteCtorSasl, AMQPHeader.getSASLHeader());
+        assertNotEquals(byteCtor, AMQPHeader.getSASLHeader());
+        assertNotEquals(defaultCtor, AMQPHeader.getSASLHeader());
+        assertEquals(byteCtorSasl, AMQPHeader.getSASLHeader());
+
+        assertFalse(AMQPHeader.getSASLHeader().equals(null));
+        assertFalse(AMQPHeader.getSASLHeader().equals(Boolean.TRUE));
+    }
+
+    @Test
+    public void testToStringOnDefault() {
+        AMQPHeader header = new AMQPHeader();
+        assertTrue(header.toString().startsWith("AMQP"));
+    }
+
+    @Test
+    public void testValidateByteWithValidHeaderBytes() {
+        ProtonBuffer buffer = AMQPHeader.getAMQPHeader().getBuffer();
+
+        byte[] bytes = buffer.getArray();
+
+        for (int i = 0; i < AMQPHeader.HEADER_SIZE_BYTES; ++i) {
+            AMQPHeader.validateByte(i, bytes[i]);
+        }
+    }
+
+    @Test
+    public void testValidateByteWithInvalidHeaderBytes() {
+        byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
+
+        for (int i = 0; i < AMQPHeader.HEADER_SIZE_BYTES; ++i) {
+            try {
+                AMQPHeader.validateByte(i, bytes[i]);
+                fail("Should throw IllegalArgumentException as bytes are invalid");
+            } catch (IllegalArgumentException iae) {
+                // Expected
+            }
+        }
+    }
+
+    @Test
+    public void testCreateWithNullBuffer() {
+        assertThrows(NullPointerException.class, () -> new AMQPHeader((ProtonBuffer) null));
+    }
+
+    @Test
+    public void testCreateWithNullByte() {
+        assertThrows(NullPointerException.class, () -> new AMQPHeader((byte[]) null));
+    }
+
+    @Test
+    public void testCreateWithEmptyBuffer() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(ProtonByteBufferAllocator.DEFAULT.allocate()));
+    }
+
+    @Test
+    public void testCreateWithOversizedBuffer() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderPrefix() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 0, 0, 1, 0, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderProtocol() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 4, 1, 0, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderMajor() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 2, 0, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderMinor() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 1, 0}));
+    }
+
+    @Test
+    public void testCreateWithInvalidHeaderRevision() {
+        assertThrows(IllegalArgumentException.class, () -> new AMQPHeader(new byte[] {'A', 'M', 'Q', 'P', 0, 1, 0, 1}));
+    }
+
+    @Test
+    public void testValidateHeaderByte0WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(0, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte1WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(1, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte2WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(2, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte3WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(3, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte4WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(4, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte5WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(5, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte6WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(6, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByte7WithInvalidValue() {
+        assertThrows(IllegalArgumentException.class, () -> AMQPHeader.validateByte(7, (byte) 85));
+    }
+
+    @Test
+    public void testValidateHeaderByteIndexOutOfBounds() {
+        assertThrows(IndexOutOfBoundsException.class, () -> AMQPHeader.validateByte(9, (byte) 65));
+    }
+
+    @Test
+    public void testInvokeOnAMQPHeader() {
+        final AtomicBoolean amqpHeader = new AtomicBoolean();
+        final AtomicBoolean saslHeader = new AtomicBoolean();
+        final AtomicReference<String> captured = new AtomicReference<>();
+
+        AMQPHeader.getAMQPHeader().invoke(new HeaderHandler<String>() {
+
+            @Override
+            public void handleAMQPHeader(AMQPHeader header, String context) {
+                amqpHeader.set(true);
+                captured.set(context);
+            }
+
+            @Override
+            public void handleSASLHeader(AMQPHeader header, String context) {
+                saslHeader.set(true);
+                captured.set(context);
+            }
+        }, "test");
+
+        assertTrue(amqpHeader.get());
+        assertFalse(saslHeader.get());
+        assertEquals("test", captured.get());
+    }
+
+    @Test
+    public void testInvokeOnSASLHeader() {
+        final AtomicBoolean amqpHeader = new AtomicBoolean();
+        final AtomicBoolean saslHeader = new AtomicBoolean();
+        final AtomicReference<String> captured = new AtomicReference<>();
+
+        AMQPHeader.getSASLHeader().invoke(new HeaderHandler<String>() {
+
+            @Override
+            public void handleAMQPHeader(AMQPHeader header, String context) {
+                amqpHeader.set(true);
+                captured.set(context);
+            }
+
+            @Override
+            public void handleSASLHeader(AMQPHeader header, String context) {
+                saslHeader.set(true);
+                captured.set(context);
+            }
+        }, "test");
+
+        assertTrue(saslHeader.get());
+        assertFalse(amqpHeader.get());
+        assertEquals("test", captured.get());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/AttachTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/AttachTest.java
new file mode 100644
index 0000000..a27afa0
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/AttachTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Binary;
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.apache.qpid.protonj2.types.messaging.Source;
+import org.apache.qpid.protonj2.types.messaging.Target;
+import org.apache.qpid.protonj2.types.transactions.Coordinator;
+import org.junit.jupiter.api.Test;
+
+public class AttachTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.ATTACH, new Attach().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Attach().toString());
+    }
+
+    @Test
+    public void testInitialState() {
+        Attach attach = new Attach();
+
+        assertEquals(0, attach.getElementCount());
+        assertTrue(attach.isEmpty());
+        assertFalse(attach.hasDesiredCapabilites());
+        assertFalse(attach.hasHandle());
+        assertFalse(attach.hasIncompleteUnsettled());
+        assertFalse(attach.hasInitialDeliveryCount());
+        assertFalse(attach.hasMaxMessageSize());
+        assertFalse(attach.hasName());
+        assertFalse(attach.hasOfferedCapabilites());
+        assertFalse(attach.hasProperties());
+        assertFalse(attach.hasReceiverSettleMode());
+        assertFalse(attach.hasRole());
+        assertFalse(attach.hasSenderSettleMode());
+        assertFalse(attach.hasSource());
+        assertFalse(attach.hasTarget());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Attach attach = new Attach();
+
+        assertEquals(0, attach.getElementCount());
+        assertTrue(attach.isEmpty());
+        assertFalse(attach.hasHandle());
+
+        attach.setHandle(0);
+
+        assertTrue(attach.getElementCount() > 0);
+        assertFalse(attach.isEmpty());
+        assertTrue(attach.hasHandle());
+
+        attach.setHandle(1);
+
+        assertTrue(attach.getElementCount() > 0);
+        assertFalse(attach.isEmpty());
+        assertTrue(attach.hasHandle());
+    }
+
+    @Test
+    public void testSetNameRefusesNull() {
+        try {
+            new Attach().setName(null);
+            fail("Link name is mandatory");
+        } catch (NullPointerException npe) {
+        }
+    }
+
+    @Test
+    public void testSetRoleRefusesNull() {
+        try {
+            new Attach().setRole(null);
+            fail("Link role is mandatory");
+        } catch (NullPointerException npe) {
+        }
+    }
+
+    @Test
+    public void testHandleRangeChecks() {
+        Attach attach = new Attach();
+        try {
+            attach.setHandle(-1l);
+            fail("Cannot set negative long handle value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            attach.setHandle(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Cannot set long handle value bigger than uint max");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testDeliveryCountRangeChecks() {
+        Attach attach = new Attach();
+        try {
+            attach.setInitialDeliveryCount(-1l);
+            fail("Cannot set negative long delivery count value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            attach.setInitialDeliveryCount(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Cannot set long delivery count value bigger than uint max");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testHasTargetOrCoordinator() {
+        Attach attach = new Attach();
+
+        assertFalse(attach.hasCoordinator());
+        assertFalse(attach.hasTarget());
+        assertFalse(attach.hasTargetOrCoordinator());
+
+        attach.setTarget(new Target());
+
+        assertFalse(attach.hasCoordinator());
+        assertTrue(attach.hasTarget());
+        assertTrue(attach.hasTargetOrCoordinator());
+
+        attach.setTarget(new Coordinator());
+
+        assertTrue(attach.hasCoordinator());
+        assertFalse(attach.hasTarget());
+        assertTrue(attach.hasTargetOrCoordinator());
+
+        attach.setTarget((Target) null);
+
+        assertFalse(attach.hasCoordinator());
+        assertFalse(attach.hasTarget());
+        assertFalse(attach.hasTargetOrCoordinator());
+
+        attach.setCoordinator(new Coordinator());
+
+        assertTrue(attach.hasCoordinator());
+        assertFalse(attach.hasTarget());
+        assertTrue(attach.hasTargetOrCoordinator());
+    }
+
+    @Test
+    public void testCopyAttachWithTarget() {
+        Attach original = new Attach();
+
+        original.setTarget(new Target());
+
+        Attach copy = original.copy();
+
+        assertNotNull(copy.getTarget());
+        assertEquals(original.<Target>getTarget(), copy.<Target>getTarget());
+    }
+
+    @Test
+    public void testCopy() {
+        final Map<Binary, DeliveryState> unsettled = new HashMap<>();
+        unsettled.put(new Binary(new byte[] {1}), Accepted.getInstance());
+
+        final Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), "test1");
+
+        Attach original = new Attach();
+
+        original.setDesiredCapabilities(Symbol.valueOf("queue"));
+        original.setOfferedCapabilities(Symbol.valueOf("queue"), Symbol.valueOf("topic"));
+        original.setHandle(1);
+        original.setIncompleteUnsettled(true);
+        original.setUnsettled(unsettled);
+        original.setInitialDeliveryCount(12);
+        original.setName("test");
+        original.setTarget(new Target());
+        original.setSource(new Source());
+        original.setRole(Role.RECEIVER);
+        original.setSenderSettleMode(SenderSettleMode.SETTLED);
+        original.setReceiverSettleMode(ReceiverSettleMode.SECOND);
+        original.setMaxMessageSize(1024);
+        original.setProperties(properties);
+
+        assertNotNull(original.toString());  // Check no fumble on full populated fields.
+
+        Attach copy = original.copy();
+
+        assertArrayEquals(copy.getDesiredCapabilities(), copy.getDesiredCapabilities());
+        assertArrayEquals(copy.getOfferedCapabilities(), copy.getOfferedCapabilities());
+        assertEquals(original.<Target>getTarget(), copy.<Target>getTarget());
+        assertEquals(original.getIncompleteUnsettled(), copy.getIncompleteUnsettled());
+        assertEquals(original.getUnsettled(), copy.getUnsettled());
+        assertEquals(original.getInitialDeliveryCount(), copy.getInitialDeliveryCount());
+        assertEquals(original.getName(), copy.getName());
+        assertEquals(original.getSource(), copy.getSource());
+        assertEquals(original.getRole(), copy.getRole());
+        assertEquals(original.getSenderSettleMode(), copy.getSenderSettleMode());
+        assertEquals(original.getReceiverSettleMode(), copy.getReceiverSettleMode());
+        assertEquals(original.getMaxMessageSize(), copy.getMaxMessageSize());
+        assertEquals(original.getProperties(), copy.getProperties());
+    }
+
+    @Test
+    public void testHasFields() {
+        final Map<Binary, DeliveryState> unsettled = new HashMap<>();
+        unsettled.put(new Binary(new byte[] {1}), Accepted.getInstance());
+        final Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), "test1");
+
+        Attach original = new Attach();
+
+        original.setDesiredCapabilities(Symbol.valueOf("queue"));
+        original.setOfferedCapabilities(Symbol.valueOf("queue"), Symbol.valueOf("topic"));
+        original.setHandle(1);
+        original.setIncompleteUnsettled(true);
+        original.setUnsettled(unsettled);
+        original.setInitialDeliveryCount(12);
+        original.setName("test");
+        original.setTarget(new Target());
+        original.setSource(new Source());
+        original.setRole(Role.RECEIVER);
+        original.setSenderSettleMode(SenderSettleMode.SETTLED);
+        original.setReceiverSettleMode(ReceiverSettleMode.SECOND);
+        original.setMaxMessageSize(1024);
+        original.setProperties(properties);
+
+        assertTrue(original.hasDesiredCapabilites());
+        assertTrue(original.hasOfferedCapabilites());
+        assertTrue(original.hasHandle());
+        assertTrue(original.hasIncompleteUnsettled());
+        assertTrue(original.hasUnsettled());
+        assertTrue(original.hasInitialDeliveryCount());
+        assertTrue(original.hasName());
+        assertTrue(original.hasTarget());
+        assertFalse(original.hasCoordinator());
+        assertTrue(original.hasSource());
+        assertTrue(original.hasRole());
+        assertTrue(original.hasSenderSettleMode());
+        assertTrue(original.hasReceiverSettleMode());
+        assertTrue(original.hasMaxMessageSize());
+        assertTrue(original.hasProperties());
+
+        original.setProperties(null);
+        original.setSource(null);
+        original.setTarget((Coordinator) null);
+        original.setMaxMessageSize(null);
+        original.setUnsettled(null);
+        original.setOfferedCapabilities(null);
+        original.setDesiredCapabilities(null);
+
+        assertFalse(original.hasTarget());
+        assertFalse(original.hasSource());
+        assertFalse(original.hasMaxMessageSize());
+        assertFalse(original.hasProperties());
+        assertFalse(original.hasUnsettled());
+
+        original.setCoordinator(new Coordinator());
+        assertFalse(original.hasTarget());
+        assertTrue(original.hasCoordinator());
+        original.setCoordinator(null);
+        assertFalse(original.hasTarget());
+        assertFalse(original.hasCoordinator());
+        assertFalse(original.hasDesiredCapabilites());
+        assertFalse(original.hasOfferedCapabilites());
+    }
+
+    @Test
+    public void testSetTargetAndCoordinatorThrowIllegalArguementErrorOnBadInput() {
+        Attach original = new Attach();
+
+        assertThrows(IllegalArgumentException.class, () -> original.setTarget(new Source()));
+    }
+
+    @Test
+    public void testReplaceTargetWithCoordinator() {
+        Attach original = new Attach();
+
+        assertFalse(original.hasTarget());
+        assertFalse(original.hasCoordinator());
+
+        original.setTarget(new Target());
+
+        assertTrue(original.hasTarget());
+        assertFalse(original.hasCoordinator());
+
+        original.setCoordinator(new Coordinator());
+
+        assertFalse(original.hasTarget());
+        assertTrue(original.hasCoordinator());
+    }
+
+    @Test
+    public void testReplaceCoordinatorWithTarget() {
+        Attach original = new Attach();
+
+        assertFalse(original.hasTarget());
+        assertFalse(original.hasCoordinator());
+
+        original.setCoordinator(new Coordinator());
+
+        assertFalse(original.hasTarget());
+        assertTrue(original.hasCoordinator());
+
+        original.setTarget(new Target());
+
+        assertTrue(original.hasTarget());
+        assertFalse(original.hasCoordinator());
+    }
+
+    @Test
+    public void testCopyAttachWithCoordinator() {
+        Attach original = new Attach();
+
+        original.setCoordinator(new Coordinator());
+
+        Attach copy = original.copy();
+
+        assertNotNull(copy.getTarget());
+        assertEquals(original.<Coordinator>getTarget(), copy.<Coordinator>getTarget(), "Should be equal");
+
+        Coordinator coordinator = copy.getTarget();
+
+        assertNotNull(coordinator);
+        assertEquals(original.getTarget(), coordinator);
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Attach original = new Attach();
+        Attach copy = original.copy();
+
+        assertTrue(original.isEmpty());
+        assertTrue(copy.isEmpty());
+
+        assertEquals(0, original.getElementCount());
+        assertEquals(0, copy.getElementCount());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/BeginTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/BeginTest.java
new file mode 100644
index 0000000..4ffd38a
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/BeginTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.junit.jupiter.api.Test;
+
+public class BeginTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.BEGIN, new Begin().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Begin().toString());
+    }
+
+    @Test
+    public void testHasMethods() {
+        Begin begin = new Begin();
+
+        assertFalse(begin.hasHandleMax());
+        assertFalse(begin.hasNextOutgoingId());
+        assertFalse(begin.hasDesiredCapabilites());
+        assertFalse(begin.hasOfferedCapabilites());
+        assertFalse(begin.hasOutgoingWindow());
+        assertFalse(begin.hasIncomingWindow());
+        assertFalse(begin.hasProperties());
+        assertFalse(begin.hasRemoteChannel());
+
+        begin.setDesiredCapabilities(Symbol.valueOf("test"));
+        begin.setOfferedCapabilities(Symbol.valueOf("test"));
+        begin.setHandleMax(65535);
+        begin.setIncomingWindow(255);
+        begin.setOutgoingWindow(Integer.MAX_VALUE);
+        begin.setRemoteChannel(1);
+        begin.setProperties(new HashMap<>());
+        begin.setNextOutgoingId(Short.MAX_VALUE);
+
+        assertTrue(begin.hasHandleMax());
+        assertTrue(begin.hasNextOutgoingId());
+        assertTrue(begin.hasDesiredCapabilites());
+        assertTrue(begin.hasOfferedCapabilites());
+        assertTrue(begin.hasOutgoingWindow());
+        assertTrue(begin.hasIncomingWindow());
+        assertTrue(begin.hasProperties());
+        assertTrue(begin.hasRemoteChannel());
+    }
+
+    @Test
+    public void testHandleMaxIfSetIsAlwaysPresent() {
+        Begin begin = new Begin();
+
+        assertFalse(begin.hasHandleMax());
+        begin.setHandleMax(0);
+        assertTrue(begin.hasHandleMax());
+        begin.setHandleMax(65535);
+        assertTrue(begin.hasHandleMax());
+        begin.setHandleMax(UnsignedInteger.MAX_VALUE.longValue());
+        assertTrue(begin.hasHandleMax());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Begin begin = new Begin();
+
+        assertEquals(0, begin.getElementCount());
+        assertTrue(begin.isEmpty());
+        assertFalse(begin.hasOutgoingWindow());
+
+        begin.setOutgoingWindow(1);
+
+        assertTrue(begin.getElementCount() > 0);
+        assertFalse(begin.isEmpty());
+        assertTrue(begin.hasOutgoingWindow());
+
+        begin.setOutgoingWindow(0);
+
+        assertTrue(begin.getElementCount() > 0);
+        assertFalse(begin.isEmpty());
+        assertTrue(begin.hasOutgoingWindow());
+    }
+
+    @Test
+    public void testIncomingWindowEnforcesRange() {
+        Begin begin = new Begin();
+
+        try {
+            begin.setIncomingWindow(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            begin.setIncomingWindow(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testOutgoingWindowEnforcesRange() {
+        Begin begin = new Begin();
+
+        try {
+            begin.setOutgoingWindow(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            begin.setOutgoingWindow(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testHandleMaxEnforcesRange() {
+        Begin begin = new Begin();
+
+        try {
+            begin.setHandleMax(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            begin.setHandleMax(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testNextOutgoingIdEnforcesRange() {
+        Begin begin = new Begin();
+
+        try {
+            begin.setNextOutgoingId(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            begin.setNextOutgoingId(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testRemoteChannelEnforcesRange() {
+        Begin begin = new Begin();
+
+        try {
+            begin.setRemoteChannel(-1);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            begin.setRemoteChannel(Integer.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Begin original = new Begin();
+        Begin copy = original.copy();
+
+        assertTrue(original.isEmpty());
+        assertTrue(copy.isEmpty());
+
+        assertEquals(0, original.getElementCount());
+        assertEquals(0, copy.getElementCount());
+    }
+
+    @Test
+    public void testCopyHandlesProperties() {
+        Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test1"), "one");
+        properties.put(Symbol.valueOf("test2"), "two");
+        properties.put(Symbol.valueOf("test3"), "three");
+
+        final Begin begin = new Begin();
+        begin.setProperties(properties);
+
+        final Begin copied = begin.copy();
+
+        assertTrue(begin.hasProperties());
+        assertTrue(copied.hasProperties());
+
+        assertEquals(copied.getProperties(), begin.getProperties());
+        assertEquals(copied.getProperties(), properties);
+    }
+
+    @Test
+    public void testCopyHandlesDesiredCapabilities() {
+        Symbol[] desiredCapabilities = { Symbol.valueOf("test1"),
+                                         Symbol.valueOf("test2"),
+                                         Symbol.valueOf("test3") };
+
+        final Begin begin = new Begin();
+        begin.setDesiredCapabilities(desiredCapabilities);
+
+        final Begin copied = begin.copy();
+
+        assertTrue(begin.hasDesiredCapabilites());
+        assertTrue(copied.hasDesiredCapabilites());
+
+        assertArrayEquals(copied.getDesiredCapabilities(), begin.getDesiredCapabilities());
+        assertArrayEquals(copied.getDesiredCapabilities(), desiredCapabilities);
+    }
+
+    @Test
+    public void testCopyHandlesOfferedCapabilities() {
+        Symbol[] offeredCapabilities = { Symbol.valueOf("test1"),
+                                         Symbol.valueOf("test2"),
+                                         Symbol.valueOf("test3") };
+
+        final Begin begin = new Begin();
+        begin.setOfferedCapabilities(offeredCapabilities);
+
+        final Begin copied = begin.copy();
+
+        assertTrue(begin.hasOfferedCapabilites());
+        assertTrue(copied.hasOfferedCapabilites());
+
+        assertArrayEquals(copied.getOfferedCapabilities(), begin.getOfferedCapabilities());
+        assertArrayEquals(copied.getOfferedCapabilities(), offeredCapabilities);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/CloseTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/CloseTest.java
new file mode 100644
index 0000000..37bb33c
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/CloseTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+import org.junit.jupiter.api.Test;
+
+public class CloseTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.CLOSE, new Close().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Close().toString());
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Close original = new Close();
+        Close copy = original.copy();
+
+        assertEquals(original.getError(), copy.getError());
+    }
+
+    @Test
+    public void testCopyWithError() {
+        Close original = new Close();
+        original.setError(new ErrorCondition(AmqpError.DECODE_ERROR, "test"));
+
+        Close copy = original.copy();
+
+        assertNotSame(copy.getError(), original.getError());
+        assertEquals(original.getError(), copy.getError());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/DetachTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/DetachTest.java
new file mode 100644
index 0000000..5a4ff59
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/DetachTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class DetachTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.DETACH, new Detach().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Detach().toString());
+    }
+
+    @Test
+    public void testDetachIsPresentChecks() {
+        Detach detach = new Detach();
+
+        assertTrue(detach.isEmpty());
+        assertFalse(detach.hasClosed());
+        assertFalse(detach.hasError());
+        assertFalse(detach.hasHandle());
+
+        detach.setClosed(false);
+        detach.setHandle(1);
+        detach.setError(new ErrorCondition("error", "error"));
+
+        assertFalse(detach.isEmpty());
+        assertTrue(detach.hasClosed());
+        assertTrue(detach.hasError());
+        assertTrue(detach.hasHandle());
+    }
+
+    @Test
+    public void testSetHandleEnforcesLimits() {
+        Detach detach = new Detach();
+
+        assertThrows(IllegalArgumentException.class, () -> detach.setHandle(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> detach.setHandle(-1l));
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Detach original = new Detach();
+        Detach copy = original.copy();
+
+        assertEquals(original.getClosed(), copy.getClosed());
+        assertEquals(original.getError(), copy.getError());
+    }
+
+    @Test
+    public void testCopyWithError() {
+        Detach original = new Detach();
+        original.setError(new ErrorCondition(AmqpError.DECODE_ERROR, "test"));
+
+        Detach copy = original.copy();
+
+        assertNotSame(copy.getError(), original.getError());
+        assertEquals(original.getError(), copy.getError());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/DispositionTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/DispositionTest.java
new file mode 100644
index 0000000..db4c344
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/DispositionTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.types.UnsignedInteger;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.junit.jupiter.api.Test;
+
+public class DispositionTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.DISPOSITION, new Disposition().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Disposition().toString());
+    }
+
+    @Test
+    public void testLastValueRangeChecks() {
+        Disposition disposition = new Disposition();
+
+        disposition.setLast(0);
+        disposition.setLast(Integer.MAX_VALUE);
+        disposition.setLast(UnsignedInteger.MAX_VALUE.longValue());
+
+        try {
+            disposition.setLast(-1l);
+            fail("Should throw on value out of range.");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            disposition.setLast(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Should throw on value out of range.");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testFirstValueRangeChecks() {
+        Disposition disposition = new Disposition();
+
+        disposition.setFirst(0);
+        disposition.setFirst(Integer.MAX_VALUE);
+        disposition.setFirst(UnsignedInteger.MAX_VALUE.longValue());
+
+        try {
+            disposition.setFirst(-1l);
+            fail("Should throw on value out of range.");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            disposition.setFirst(UnsignedInteger.MAX_VALUE.longValue() + 1);
+            fail("Should throw on value out of range.");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testRoleCannotBeSetNull() {
+        Disposition disposition = new Disposition();
+
+        assertThrows(NullPointerException.class, () -> disposition.setRole(null));
+    }
+
+    @Test
+    public void testInitialState() {
+        Disposition disposition = new Disposition();
+
+        assertEquals(0, disposition.getElementCount());
+        assertTrue(disposition.isEmpty());
+        assertFalse(disposition.hasBatchable());
+        assertFalse(disposition.hasFirst());
+        assertFalse(disposition.hasLast());
+        assertFalse(disposition.hasRole());
+        assertFalse(disposition.hasSettled());
+        assertFalse(disposition.hasState());
+    }
+
+    @Test
+    public void testClearPayloadAPI() {
+        Disposition disposition = new Disposition();
+
+        disposition.setBatchable(true);
+        disposition.setFirst(1);
+        disposition.setLast(2);
+        disposition.setRole(Role.SENDER);
+        disposition.setSettled(true);
+        disposition.setState(Accepted.getInstance());
+
+        assertFalse(disposition.isEmpty());
+        assertTrue(disposition.hasBatchable());
+        assertTrue(disposition.hasFirst());
+        assertTrue(disposition.hasLast());
+        assertTrue(disposition.hasRole());
+        assertTrue(disposition.hasSettled());
+        assertTrue(disposition.hasState());
+
+        disposition.clearBatchable();
+        disposition.clearFirst();
+        disposition.clearLast();
+        disposition.clearRole();
+        disposition.clearSettled();
+        disposition.clearState();
+
+        assertEquals(0, disposition.getElementCount());
+        assertTrue(disposition.isEmpty());
+        assertFalse(disposition.hasBatchable());
+        assertFalse(disposition.hasFirst());
+        assertFalse(disposition.hasLast());
+        assertFalse(disposition.hasRole());
+        assertFalse(disposition.hasSettled());
+        assertFalse(disposition.hasState());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Disposition disposition = new Disposition();
+
+        assertEquals(0, disposition.getElementCount());
+        assertTrue(disposition.isEmpty());
+        assertFalse(disposition.hasFirst());
+
+        disposition.setFirst(0);
+
+        assertTrue(disposition.getElementCount() > 0);
+        assertFalse(disposition.isEmpty());
+        assertTrue(disposition.hasFirst());
+
+        disposition.setFirst(1);
+
+        assertTrue(disposition.getElementCount() > 0);
+        assertFalse(disposition.isEmpty());
+        assertTrue(disposition.hasFirst());
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Disposition original = new Disposition();
+        Disposition copy = original.copy();
+
+        assertTrue(original.isEmpty());
+        assertTrue(copy.isEmpty());
+
+        assertEquals(0, original.getElementCount());
+        assertEquals(0, copy.getElementCount());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/EndTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/EndTest.java
new file mode 100644
index 0000000..15d0d99
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/EndTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+import org.junit.jupiter.api.Test;
+
+public class EndTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.END, new End().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new End().toString());
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        End original = new End();
+        End copy = original.copy();
+
+        assertEquals(original.getError(), copy.getError());
+    }
+
+    @Test
+    public void testCopyWithError() {
+        End original = new End();
+        original.setError(new ErrorCondition(AmqpError.DECODE_ERROR, "test"));
+
+        End copy = original.copy();
+
+        assertNotSame(copy.getError(), original.getError());
+        assertEquals(original.getError(), copy.getError());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/ErrorConditionTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/ErrorConditionTest.java
new file mode 100644
index 0000000..eb3fc7f
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/ErrorConditionTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class ErrorConditionTest {
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new ErrorCondition(AmqpError.DECODE_ERROR, (String) null).toString());
+    }
+
+    @Test
+    public void testEqualsWithCreateFromStringVsCreateFromSymbolCondition() {
+        ErrorCondition fromString = new ErrorCondition(AmqpError.DECODE_ERROR.toString(), "error");
+        ErrorCondition fromSymbol = new ErrorCondition(AmqpError.DECODE_ERROR, "error");
+
+        assertEquals(fromString, fromSymbol);
+    }
+
+    @SuppressWarnings("unlikely-arg-type")
+    @Test
+    public void testEquals() {
+        ErrorCondition original = new ErrorCondition(AmqpError.DECODE_ERROR, "error");
+        ErrorCondition copy = original.copy();
+
+        assertEquals(original, copy);
+
+        Map<Symbol, Object> infoMap = new HashMap<>();
+        ErrorCondition other1 = new ErrorCondition(null, "error", infoMap);
+        ErrorCondition other2 = new ErrorCondition(AmqpError.DECODE_ERROR, null, infoMap);
+        ErrorCondition other3 = new ErrorCondition(AmqpError.DECODE_ERROR, "error", infoMap);
+        ErrorCondition other4 = new ErrorCondition(null, null, infoMap);
+        ErrorCondition other5 = new ErrorCondition(null, null, null);
+
+        assertNotEquals(original, other1);
+        assertNotEquals(original, other2);
+        assertNotEquals(original, other3);
+        assertNotEquals(original, other4);
+        assertNotEquals(original, other5);
+
+        assertNotEquals(other1, original);
+        assertNotEquals(other2, original);
+        assertNotEquals(other3, original);
+        assertNotEquals(other4, original);
+        assertNotEquals(other5, original);
+
+        assertFalse(original.equals(null));
+        assertFalse(original.equals(Boolean.TRUE));
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        ErrorCondition original = new ErrorCondition(AmqpError.DECODE_ERROR, "error");
+        ErrorCondition copy = original.copy();
+
+        assertEquals(original.getCondition(), copy.getCondition());
+        assertEquals(original.getDescription(), copy.getDescription());
+        assertEquals(original.getInfo(), copy.getInfo());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/FlowTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/FlowTest.java
new file mode 100644
index 0000000..2ee1c76
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/FlowTest.java
@@ -0,0 +1,260 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class FlowTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.FLOW, new Flow().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Flow().toString());
+    }
+
+    @Test
+    public void testSetHandleEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setHandle(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setHandle(-1l));
+    }
+
+    @Test
+    public void testSetNextIncomingIdEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setNextIncomingId(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setNextIncomingId(-1l));
+    }
+
+    @Test
+    public void testSetNextOutgoingIdEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setNextOutgoingId(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setNextOutgoingId(-1l));
+    }
+
+    @Test
+    public void testSetOutgoingWindowEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setOutgoingWindow(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setOutgoingWindow(-1l));
+    }
+
+    @Test
+    public void testSetIncomingWindowEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setIncomingWindow(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setIncomingWindow(-1l));
+    }
+
+    @Test
+    public void testSetDeliveryCountEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setDeliveryCount(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setDeliveryCount(-1l));
+    }
+
+    @Test
+    public void testSetLinkCreditEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setLinkCredit(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setLinkCredit(-1l));
+    }
+
+    @Test
+    public void testSetAvaiableEnforcesLimits() {
+        Flow flow = new Flow();
+
+        assertThrows(IllegalArgumentException.class, () -> flow.setAvailable(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> flow.setAvailable(-1l));
+    }
+
+    @Test
+    public void testCopy() {
+        final Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), "test1");
+
+        Flow flow = new Flow();
+
+        flow.setAvailable(1024);
+        flow.setDeliveryCount(5);
+        flow.setDrain(true);
+        flow.setEcho(true);
+        flow.setHandle(3);
+        flow.setIncomingWindow(1024);
+        flow.setLinkCredit(255);
+        flow.setNextIncomingId(12);
+        flow.setNextOutgoingId(13);
+        flow.setOutgoingWindow(2048);
+        flow.setProperties(properties);
+
+        Flow copy = flow.copy();
+
+        assertEquals(flow.getAvailable(), copy.getAvailable());
+        assertEquals(flow.getDeliveryCount(), copy.getDeliveryCount());
+        assertEquals(flow.getDrain(), copy.getDrain());
+        assertEquals(flow.getEcho(), copy.getEcho());
+        assertEquals(flow.getHandle(), copy.getHandle());
+        assertEquals(flow.getIncomingWindow(), copy.getIncomingWindow());
+        assertEquals(flow.getOutgoingWindow(), copy.getOutgoingWindow());
+        assertEquals(flow.getNextIncomingId(), copy.getNextIncomingId());
+        assertEquals(flow.getNextOutgoingId(), copy.getNextOutgoingId());
+        assertEquals(flow.getProperties(), copy.getProperties());
+        assertEquals(flow.getLinkCredit(), copy.getLinkCredit());
+    }
+
+    @Test
+    public void testClearFieldsAPI() {
+        final Map<Symbol, Object> properties = new HashMap<>();
+        properties.put(Symbol.valueOf("test"), "test1");
+
+        Flow flow = new Flow();
+
+        flow.setAvailable(1024);
+        flow.setDeliveryCount(5);
+        flow.setDrain(true);
+        flow.setEcho(true);
+        flow.setHandle(3);
+        flow.setIncomingWindow(1024);
+        flow.setLinkCredit(255);
+        flow.setNextIncomingId(12);
+        flow.setNextOutgoingId(13);
+        flow.setOutgoingWindow(2048);
+        flow.setProperties(properties);
+
+        assertEquals(11, flow.getElementCount());
+        assertFalse(flow.isEmpty());
+        assertTrue(flow.hasAvailable());
+        assertTrue(flow.hasDeliveryCount());
+        assertTrue(flow.hasDrain());
+        assertTrue(flow.hasEcho());
+        assertTrue(flow.hasHandle());
+        assertTrue(flow.hasIncomingWindow());
+        assertTrue(flow.hasLinkCredit());
+        assertTrue(flow.hasNextIncomingId());
+        assertTrue(flow.hasNextOutgoingId());
+        assertTrue(flow.hasOutgoingWindow());
+        assertTrue(flow.hasProperties());
+
+        assertNotNull(flow.toString()); // Ensure fully populated toString does not error
+
+        flow.clearAvailable();
+        flow.clearDeliveryCount();
+        flow.clearDrain();
+        flow.clearEcho();
+        flow.clearHandle();
+        flow.clearIncomingWindow();
+        flow.clearLinkCredit();
+        flow.clearNextIncomingId();
+        flow.clearNextOutgoingId();
+        flow.clearOutgoingWindow();
+        flow.clearProperties();
+
+        assertEquals(0, flow.getElementCount());
+        assertTrue(flow.isEmpty());
+        assertFalse(flow.hasAvailable());
+        assertFalse(flow.hasDeliveryCount());
+        assertFalse(flow.hasDrain());
+        assertFalse(flow.hasEcho());
+        assertFalse(flow.hasHandle());
+        assertFalse(flow.hasIncomingWindow());
+        assertFalse(flow.hasLinkCredit());
+        assertFalse(flow.hasNextIncomingId());
+        assertFalse(flow.hasNextOutgoingId());
+        assertFalse(flow.hasOutgoingWindow());
+        assertFalse(flow.hasProperties());
+
+        flow.setProperties(properties);
+        assertTrue(flow.hasProperties());
+        flow.setProperties(null);
+        assertFalse(flow.hasProperties());
+    }
+
+    @Test
+    public void testInitialState() {
+        Flow flow = new Flow();
+
+        assertEquals(0, flow.getElementCount());
+        assertTrue(flow.isEmpty());
+        assertFalse(flow.hasAvailable());
+        assertFalse(flow.hasDeliveryCount());
+        assertFalse(flow.hasDrain());
+        assertFalse(flow.hasEcho());
+        assertFalse(flow.hasHandle());
+        assertFalse(flow.hasIncomingWindow());
+        assertFalse(flow.hasLinkCredit());
+        assertFalse(flow.hasNextIncomingId());
+        assertFalse(flow.hasNextOutgoingId());
+        assertFalse(flow.hasOutgoingWindow());
+        assertFalse(flow.hasProperties());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Flow flow = new Flow();
+
+        assertEquals(0, flow.getElementCount());
+        assertTrue(flow.isEmpty());
+        assertFalse(flow.hasLinkCredit());
+
+        flow.setLinkCredit(10);
+
+        assertNotNull(flow.toString()); // Ensure partially populated toString does not error
+        assertTrue(flow.getElementCount() > 0);
+        assertFalse(flow.isEmpty());
+        assertTrue(flow.hasLinkCredit());
+
+        flow.setLinkCredit(0);
+
+        assertTrue(flow.getElementCount() > 0);
+        assertFalse(flow.isEmpty());
+        assertTrue(flow.hasLinkCredit());
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Flow original = new Flow();
+        Flow copy = original.copy();
+
+        assertTrue(original.isEmpty());
+        assertTrue(copy.isEmpty());
+
+        assertEquals(0, original.getElementCount());
+        assertEquals(0, copy.getElementCount());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/OpenTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/OpenTest.java
new file mode 100644
index 0000000..a3dbaf8
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/OpenTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.qpid.protonj2.types.Symbol;
+import org.junit.jupiter.api.Test;
+
+public class OpenTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.OPEN, new Open().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Open().toString());
+    }
+
+    @Test
+    public void testMaxFrameSizeLimitsImposed() {
+        Open open = new Open();
+
+        assertThrows(IllegalArgumentException.class, () -> open.setMaxFrameSize(Long.MAX_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> open.setMaxFrameSize(-1l));
+    }
+
+    @Test
+    public void testSetGetOfOutgoingLocales() {
+        Open open = new Open();
+        Symbol[] expected = { Symbol.valueOf("1"), Symbol.valueOf("2") };
+
+        open.setOutgoingLocales(Symbol.valueOf("1"), Symbol.valueOf("2"));
+
+        assertArrayEquals(expected, open.getOutgoingLocales());
+        assertNotNull(open.toString());
+    }
+
+    @Test
+    public void testSetGetOfIncomingLocales() {
+        Open open = new Open();
+        Symbol[] expected = { Symbol.valueOf("1"), Symbol.valueOf("2") };
+
+        open.setIncomingLocales(Symbol.valueOf("1"), Symbol.valueOf("2"));
+
+        assertArrayEquals(expected, open.getIncomingLocales());
+        assertNotNull(open.toString());
+    }
+
+    @Test
+    public void testSetChannelMaxFromShort() {
+        Open open = new Open();
+
+        open.setChannelMax((short) 65535);
+        assertEquals(65535, open.getChannelMax());
+        open.setChannelMax((short) -1);
+        assertEquals(65535, open.getChannelMax());
+        open.setChannelMax((short) 0);
+        assertEquals(0, open.getChannelMax());
+    }
+
+    @Test
+    public void testInitialState() {
+        Open open = new Open();
+
+        assertEquals(1, open.getElementCount());
+        assertFalse(open.isEmpty());
+        assertFalse(open.hasChannelMax());
+        assertTrue(open.hasContainerId());
+        assertFalse(open.hasDesiredCapabilites());
+        assertFalse(open.hasHostname());
+        assertFalse(open.hasIdleTimeout());
+        assertFalse(open.hasIncomingLocales());
+        assertFalse(open.hasMaxFrameSize());
+        assertFalse(open.hasOfferedCapabilites());
+        assertFalse(open.hasOutgoingLocales());
+        assertFalse(open.hasProperties());
+    }
+
+    @Test
+    public void testChannelMaxAlwaysPresentOnceSet() {
+        Open open = new Open();
+
+        assertFalse(open.hasChannelMax());
+        open.setChannelMax(0);
+        assertTrue(open.hasChannelMax());
+        open.setChannelMax(32767);
+        assertTrue(open.hasChannelMax());
+        open.setChannelMax(65535);
+        assertTrue(open.hasChannelMax());
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Open original = new Open();
+        Open copy = original.copy();
+
+        assertFalse(original.isEmpty());
+        assertFalse(copy.isEmpty());
+
+        assertEquals(1, original.getElementCount());
+        assertEquals(1, copy.getElementCount());
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/ReceiverSettleModeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/ReceiverSettleModeTest.java
new file mode 100644
index 0000000..6da9bac
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/ReceiverSettleModeTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.junit.jupiter.api.Test;
+
+public class ReceiverSettleModeTest {
+
+    @Test
+    public void testValueOf() {
+        assertEquals(ReceiverSettleMode.FIRST, ReceiverSettleMode.valueOf((UnsignedByte) null));
+        assertEquals(ReceiverSettleMode.FIRST, ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 0)));
+        assertEquals(ReceiverSettleMode.SECOND, ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 1)));
+    }
+
+    @Test
+    public void testEquality() {
+        ReceiverSettleMode first = ReceiverSettleMode.FIRST;
+        ReceiverSettleMode second = ReceiverSettleMode.SECOND;
+
+        assertEquals(first, ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 0)));
+        assertEquals(second, ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 1)));
+
+        assertEquals(first.getValue(), UnsignedByte.valueOf((byte) 0));
+        assertEquals(second.getValue(), UnsignedByte.valueOf((byte) 1));
+    }
+
+    @Test
+    public void testNotEquality() {
+        ReceiverSettleMode first = ReceiverSettleMode.FIRST;
+        ReceiverSettleMode second = ReceiverSettleMode.SECOND;
+
+        assertNotEquals(first, ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 1)));
+        assertNotEquals(second, ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 0)));
+
+        assertNotEquals(first.getValue(), UnsignedByte.valueOf((byte) 1));
+        assertNotEquals(second.getValue(), UnsignedByte.valueOf((byte) 0));
+    }
+
+    @Test
+    public void testIllegalArgument() {
+        assertThrows(IllegalArgumentException.class, () -> ReceiverSettleMode.valueOf(UnsignedByte.valueOf((byte) 2)));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/RoleTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/RoleTest.java
new file mode 100644
index 0000000..41a7e65
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/RoleTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class RoleTest {
+
+    @Test
+    public void testValueOf() {
+        assertEquals(Role.SENDER, Role.valueOf((Boolean) null));
+        assertEquals(Role.SENDER, Role.valueOf(Boolean.FALSE));
+        assertEquals(Role.RECEIVER, Role.valueOf(Boolean.TRUE));
+        assertEquals(Role.SENDER, Role.valueOf(false));
+        assertEquals(Role.RECEIVER, Role.valueOf(true));
+    }
+
+    @Test
+    public void testEquality() {
+        Role sender = Role.SENDER;
+        Role receiver = Role.RECEIVER;
+
+        assertEquals(sender, Role.valueOf(false));
+        assertEquals(receiver, Role.valueOf(true));
+
+        assertEquals(sender.getValue(), Boolean.FALSE);
+        assertEquals(receiver.getValue(), Boolean.TRUE);
+    }
+
+    @Test
+    public void testNotEquality() {
+        Role sender = Role.SENDER;
+        Role receiver = Role.RECEIVER;
+
+        assertNotEquals(sender, Role.valueOf(true));
+        assertNotEquals(receiver, Role.valueOf(false));
+
+        assertNotEquals(sender.getValue(), Boolean.TRUE);
+        assertNotEquals(receiver.getValue(), Boolean.FALSE);
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/SenderSettleModeTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/SenderSettleModeTest.java
new file mode 100644
index 0000000..f2b6c59
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/SenderSettleModeTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.qpid.protonj2.types.UnsignedByte;
+import org.junit.jupiter.api.Test;
+
+public class SenderSettleModeTest {
+
+    @Test
+    public void testValueOf() {
+        assertEquals(SenderSettleMode.MIXED, SenderSettleMode.valueOf((UnsignedByte) null));
+        assertEquals(SenderSettleMode.UNSETTLED, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 0)));
+        assertEquals(SenderSettleMode.SETTLED, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 1)));
+        assertEquals(SenderSettleMode.MIXED, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 2)));
+    }
+
+    @Test
+    public void testEquality() {
+        SenderSettleMode unsettled = SenderSettleMode.UNSETTLED;
+        SenderSettleMode settled = SenderSettleMode.SETTLED;
+        SenderSettleMode mixed = SenderSettleMode.MIXED;
+
+        assertEquals(unsettled, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 0)));
+        assertEquals(settled, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 1)));
+        assertEquals(mixed, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 2)));
+
+        assertEquals(unsettled.getValue(), UnsignedByte.valueOf((byte) 0));
+        assertEquals(settled.getValue(), UnsignedByte.valueOf((byte) 1));
+        assertEquals(mixed.getValue(), UnsignedByte.valueOf((byte) 2));
+    }
+
+    @Test
+    public void testNotEquality() {
+        SenderSettleMode unsettled = SenderSettleMode.UNSETTLED;
+        SenderSettleMode settled = SenderSettleMode.SETTLED;
+        SenderSettleMode mixed = SenderSettleMode.MIXED;
+
+        assertNotEquals(unsettled, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 2)));
+        assertNotEquals(settled, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 0)));
+        assertNotEquals(mixed, SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 1)));
+
+        assertNotEquals(unsettled.getValue(), UnsignedByte.valueOf((byte) 2));
+        assertNotEquals(settled.getValue(), UnsignedByte.valueOf((byte) 0));
+        assertNotEquals(mixed.getValue(), UnsignedByte.valueOf((byte) 1));
+    }
+
+    @Test
+    public void testIllegalArgument() {
+        assertThrows(IllegalArgumentException.class, () -> SenderSettleMode.valueOf(UnsignedByte.valueOf((byte) 3)));
+    }
+}
diff --git a/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/TransferTest.java b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/TransferTest.java
new file mode 100644
index 0000000..49e4867
--- /dev/null
+++ b/protonj2/src/test/java/org/apache/qpid/protonj2/types/transport/TransferTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+package org.apache.qpid.protonj2.types.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.types.DeliveryTag;
+import org.apache.qpid.protonj2.types.messaging.Accepted;
+import org.junit.jupiter.api.Test;
+
+public class TransferTest {
+
+    @Test
+    public void testGetPerformativeType() {
+        assertEquals(Performative.PerformativeType.TRANSFER, new Transfer().getPerformativeType());
+    }
+
+    @Test
+    public void testToStringOnFreshInstance() {
+        assertNotNull(new Transfer().toString());
+    }
+
+    @Test
+    public void testInitialState() {
+        Transfer transfer = new Transfer();
+
+        assertEquals(0, transfer.getElementCount());
+        assertTrue(transfer.isEmpty());
+        assertFalse(transfer.hasAborted());
+        assertFalse(transfer.hasBatchable());
+        assertFalse(transfer.hasDeliveryId());
+        assertFalse(transfer.hasDeliveryTag());
+        assertFalse(transfer.hasHandle());
+        assertFalse(transfer.hasMessageFormat());
+        assertFalse(transfer.hasMore());
+        assertFalse(transfer.hasRcvSettleMode());
+        assertFalse(transfer.hasResume());
+        assertFalse(transfer.hasSettled());
+        assertFalse(transfer.hasState());
+    }
+
+    @Test
+    public void testClearFieldsAPI() {
+        Transfer transfer = new Transfer();
+
+        transfer.setAborted(true);
+        transfer.setBatchable(true);
+        transfer.setDeliveryId(1);
+        transfer.setDeliveryTag(new byte[] { 1 });
+        transfer.setHandle(2);
+        transfer.setMessageFormat(12);
+        transfer.setMore(true);
+        transfer.setRcvSettleMode(ReceiverSettleMode.SECOND);
+        transfer.setResume(true);
+        transfer.setSettled(true);
+        transfer.setState(Accepted.getInstance());
+
+        assertNotNull(transfer.toString());
+        assertEquals(11, transfer.getElementCount());
+        assertFalse(transfer.isEmpty());
+        assertTrue(transfer.hasAborted());
+        assertTrue(transfer.hasBatchable());
+        assertTrue(transfer.hasDeliveryId());
+        assertTrue(transfer.hasDeliveryTag());
+        assertTrue(transfer.hasHandle());
+        assertTrue(transfer.hasMessageFormat());
+        assertTrue(transfer.hasMore());
+        assertTrue(transfer.hasRcvSettleMode());
+        assertTrue(transfer.hasResume());
+        assertTrue(transfer.hasSettled());
+        assertTrue(transfer.hasState());
+
+        transfer.clearAborted();
+        transfer.clearBatchable();
+        transfer.clearDeliveryId();
+        transfer.clearDeliveryTag();
+        transfer.clearHandle();
+        transfer.clearMessageFormat();
+        transfer.clearMore();
+        transfer.clearRcvSettleMode();
+        transfer.clearResume();
+        transfer.clearSettled();
+        transfer.clearState();
+
+        assertEquals(0, transfer.getElementCount());
+        assertTrue(transfer.isEmpty());
+        assertFalse(transfer.hasAborted());
+        assertFalse(transfer.hasBatchable());
+        assertFalse(transfer.hasDeliveryId());
+        assertFalse(transfer.hasDeliveryTag());
+        assertFalse(transfer.hasHandle());
+        assertFalse(transfer.hasMessageFormat());
+        assertFalse(transfer.hasMore());
+        assertFalse(transfer.hasRcvSettleMode());
+        assertFalse(transfer.hasResume());
+        assertFalse(transfer.hasSettled());
+        assertFalse(transfer.hasState());
+
+        transfer.setDeliveryTag(new byte[] { 1 });
+        assertTrue(transfer.hasDeliveryTag());
+        transfer.setDeliveryTag((byte[]) null);
+        assertFalse(transfer.hasDeliveryTag());
+
+        transfer.setDeliveryTag(new byte[] { 1 });
+        assertTrue(transfer.hasDeliveryTag());
+        transfer.setDeliveryTag((DeliveryTag) null);
+        assertFalse(transfer.hasDeliveryTag());
+
+        transfer.setDeliveryTag(new byte[] { 1 });
+        assertTrue(transfer.hasDeliveryTag());
+        transfer.setDeliveryTag((ProtonBuffer) null);
+        assertFalse(transfer.hasDeliveryTag());
+
+        transfer.setRcvSettleMode(ReceiverSettleMode.SECOND);
+        assertTrue(transfer.hasRcvSettleMode());
+        transfer.setRcvSettleMode(null);
+        assertFalse(transfer.hasRcvSettleMode());
+    }
+
+    @Test
+    public void testCopy() {
+        Transfer transfer = new Transfer();
+
+        transfer.setAborted(true);
+        transfer.setBatchable(true);
+        transfer.setDeliveryId(1);
+        transfer.setDeliveryTag(new byte[] { 1 });
+        transfer.setHandle(2);
+        transfer.setMessageFormat(12);
+        transfer.setMore(true);
+        transfer.setRcvSettleMode(ReceiverSettleMode.SECOND);
+        transfer.setResume(true);
+        transfer.setSettled(true);
+        transfer.setState(Accepted.getInstance());
+
+        Transfer copy = transfer.copy();
+
+        assertEquals(transfer.getAborted(), copy.getAborted());
+        assertEquals(transfer.getBatchable(), copy.getBatchable());
+        assertEquals(transfer.getDeliveryId(), copy.getDeliveryId());
+        assertEquals(transfer.getDeliveryTag(), copy.getDeliveryTag());
+        assertEquals(transfer.getHandle(), copy.getHandle());
+        assertEquals(transfer.getMessageFormat(), copy.getMessageFormat());
+        assertEquals(transfer.getMore(), copy.getMore());
+        assertEquals(transfer.getRcvSettleMode(), copy.getRcvSettleMode());
+        assertEquals(transfer.getResume(), copy.getResume());
+        assertEquals(transfer.getSettled(), copy.getSettled());
+        assertEquals(transfer.getState(), copy.getState());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        Transfer transfer = new Transfer();
+
+        assertEquals(0, transfer.getElementCount());
+        assertTrue(transfer.isEmpty());
+        assertFalse(transfer.hasAborted());
+
+        transfer.setAborted(true);
+
+        assertTrue(transfer.getElementCount() > 0);
+        assertFalse(transfer.isEmpty());
+        assertTrue(transfer.hasAborted());
+        assertTrue(transfer.getAborted());
+
+        transfer.setAborted(false);
+
+        assertNotNull(transfer.toString());
+        assertTrue(transfer.getElementCount() > 0);
+        assertFalse(transfer.isEmpty());
+        assertTrue(transfer.hasAborted());
+        assertFalse(transfer.getAborted());
+    }
+
+    @Test
+    public void testSetHandleEnforcesRange() {
+        Transfer transfer = new Transfer();
+
+        try {
+            transfer.setHandle(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            transfer.setHandle(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testSetDeliveryIdEnforcesRange() {
+        Transfer transfer = new Transfer();
+
+        try {
+            transfer.setDeliveryId(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            transfer.setDeliveryId(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testSetMessageFormatEnforcesRange() {
+        Transfer transfer = new Transfer();
+
+        try {
+            transfer.setMessageFormat(-1l);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+
+        try {
+            transfer.setMessageFormat(Long.MAX_VALUE);
+            fail("Should not be able to set out of range value");
+        } catch (IllegalArgumentException iae) {}
+    }
+
+    @Test
+    public void testCopyFromNew() {
+        Transfer original = new Transfer();
+        Transfer copy = original.copy();
+
+        assertTrue(original.isEmpty());
+        assertTrue(copy.isEmpty());
+
+        assertEquals(0, original.getElementCount());
+        assertEquals(0, copy.getElementCount());
+    }
+}
diff --git a/protonj2/src/test/resources/log4j2-test.properties b/protonj2/src/test/resources/log4j2-test.properties
new file mode 100644
index 0000000..75e8ffc
--- /dev/null
+++ b/protonj2/src/test/resources/log4j2-test.properties
@@ -0,0 +1,36 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name=ClientModuleTestPropertiesConfig
+status=warn
+
+appender.console.type=Console
+appender.console.name=STDOUT
+appender.console.layout.type=PatternLayout
+appender.console.layout.pattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n
+
+rootLogger.level=trace
+rootLogger.appenderRef.console.ref=STDOUT
+
+logger.proton.name=org.apache.qpid.protonj2
+logger.proton.level=trace
+
+logger.testpeer.name=org.apache.qpid.protonj2.test.driver
+logger.testpeer.level=trace
+